Compare commits

...

10 Commits
3.0.2 ... 3.1.1

Author SHA1 Message Date
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
37 changed files with 2768 additions and 405 deletions

View File

@@ -11,7 +11,7 @@ At TheCodingMachine, we build a lot of web applications (intranets, extranets an
## Features
* HTML and Markdown conversions using Google Chrome headless
* Office conversions (.docx, .doc, .odt, .pptx, .ppt, .odp and so on) using [unoconv](https://github.com/dagwieers/unoconv)
* 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!

View File

@@ -11,7 +11,7 @@ LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
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
apt-get install -y curl wget python3-pip
# |--------------------------------------------------------------------------
# | PM2
@@ -39,7 +39,10 @@ RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add
# | Unoconv
# |--------------------------------------------------------------------------
# |
# | Installs MS fonts and unoconv for Office documents conversions.
# | Installs unoconv and LibreOffice for Office documents conversions.
# |
RUN apt-get -y install ttf-mscorefonts-installer unoconv
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

View File

@@ -5,7 +5,7 @@ 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 (.docx, .doc, .odt, .pptx, .ppt, .odp and so on) using [unoconv](https://github.com/dagwieers/unoconv)
* 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!

View File

@@ -43,9 +43,7 @@ import "github.com/thecodingmachine/gotenberg/pkg"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req := &gotenberg.HTMLRequest{
IndexFilePath: "index.html",
}
req, _ := gotenberg.NewHTMLRequest("index.html")
dest := "result.pdf"
c.Store(req, dest)
}
@@ -119,13 +117,9 @@ import "github.com/thecodingmachine/gotenberg/pkg"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req := &gotenberg.HTMLRequest{
IndexFilePath: "index.html",
Options: &gotenberg.HTMLOptions{
HeaderFilePath: "header.html",
FooterFilePath: "footer.html",
},
}
req, _ := gotenberg.NewHTMLRequest("index.html")
req.SetHeader("header.html")
req.SetFooter("footer.html")
dest := "result.pdf"
c.Store(req, dest)
}
@@ -213,14 +207,12 @@ import "github.com/thecodingmachine/gotenberg/pkg"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req := &gotenberg.HTMLRequest{
IndexFilePath: "index.html",
AssetFilePaths: []string{
"style.css",
"img.png",
"font.woff",
},
}
req, _ := gotenberg.NewHTMLRequest("index.html")
req.SetAssets([]string{
"font.woff",
"img.gif",
"style.css",
})
dest := "result.pdf"
c.Store(req, dest)
}
@@ -279,14 +271,10 @@ import "github.com/thecodingmachine/gotenberg/pkg"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req := &gotenberg.HTMLRequest{
IndexFilePath: "index.html",
Options: &gotenberg.HTMLOptions{
PaperSize: gotenberg.A4,
PaperMargins: gotenberg.NoMargins,
Landscape: true
},
}
req, _ := gotenberg.NewHTMLRequest("index.html")
req.SetPaperSize(gotenberg.A4)
req.SetMargins(gotenberg.NoMargins)
req.SetLandscape(true)
dest := "result.pdf"
c.Store(req, dest)
}

View File

@@ -46,12 +46,7 @@ import "github.com/thecodingmachine/gotenberg/pkg"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req := &gotenberg.MarkdownRequest{
IndexFilePath: "index.html",
MarkdownFilePaths: []string{
"file.md",
},
}
req, _ := gotenberg.NewMarkdownRequest("index.html", []string{"file.md"})
dest := "result.pdf"
c.Store(req, dest)
}

View File

@@ -10,6 +10,8 @@ It accepts `POST` requests with a `multipart/form-data` Content-Type.
You may send one or more Office documents. Following file extensions are accepted:
* `.txt`
* `.rtf`
* `.doc`
* `.docx`
* `.odt`
@@ -44,12 +46,7 @@ import "github.com/thecodingmachine/gotenberg/pkg"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req := &gotenberg.OfficeRequest{
FilePaths: []string{
"document.docx",
"document2.docx",
},
}
req, _ := gotenberg.NewOfficeRequest([]string{"document.docx", "document2.docx"})
dest := "result.pdf"
c.Store(req, dest)
}
@@ -72,6 +69,61 @@ $dirPath = "/foo";
$filename = $client->store($request, $dirPath);
```
## Paper size and orientation
You may also customize the resulting PDF format.
By default, it will be rendered with `A4` size and `portrait` orientation.
> Paper size has to be provided in `inches`.
> Also, you have to set both `paperWidth` and `paperHeight`.
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/office \
--header 'Content-Type: multipart/form-data' \
--form files=@document.docx \
--form paperWidth=8.27 \
--form paperHeight=11.27 \
--form landscape=true \
> result.pdf
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg/pkg"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewOfficeRequest([]string{"document.docx"})
req.SetPaperSize(gotenberg.A4)
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->setPaperSize(Request::A4);
$request->setLandscape(true);
$dirPath = "/foo";
$filename = $client->store($request, $dirPath);
```
## Fonts
By default, only `ttf-mscorefonts` fonts are installed.

View File

@@ -29,12 +29,7 @@ import "github.com/thecodingmachine/gotenberg/pkg"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req := &gotenberg.MergeRequest{
FilePaths: []string{
"file.pdf",
"file2.pdf",
},
}
req, _ := gotenberg.NewMergeRequest([]string{"file.pdf", "file2.pdf"})
dest := "result.pdf"
c.Store(req, dest)
}

View File

@@ -28,14 +28,10 @@ import "github.com/thecodingmachine/gotenberg/pkg"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req := &gotenberg.HTMLRequest{
IndexFilePath: "index.html",
Options: &gotenberg.HTMLOptions{
WebHookURL: "http://myapp.com/webhook/",
},
}
req, _ := gotenberg.NewHTMLRequest("index.html")
req.SetWebhookURL("http://myapp.com/webhook/")
dest := "result.pdf"
resp, err := c.Post(req)
resp, _ := c.Post(req)
}
```

View File

@@ -21,4 +21,5 @@ go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvS
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
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/pkg
sleep 5 # allows Gotenberg to remove generated files (concurrent requests).

View File

@@ -71,7 +71,7 @@
<ul>
<li>HTML and Markdown conversions using Google Chrome headless</li>
<li>Office conversions (.docx, .doc, .odt, .pptx, .ppt, .odp and so on) using <a href="https://github.com/dagwieers/unoconv">unoconv</a></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>
@@ -187,9 +187,7 @@ which will be converted to PDF.</p>
<span class="kd">func</span> <span class="nx">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="o">&amp;</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">HTMLRequest</span><span class="p">{</span>
<span class="nx">IndexFilePath</span><span class="p">:</span> <span class="s">&#34;index.html&#34;</span><span class="p">,</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="nx">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="nx">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>
@@ -271,13 +269,9 @@ Also, <code>footer.html</code> CSS properties override the ones from <code>heade
<span class="kd">func</span> <span class="nx">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="o">&amp;</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">HTMLRequest</span><span class="p">{</span>
<span class="nx">IndexFilePath</span><span class="p">:</span> <span class="s">&#34;index.html&#34;</span><span class="p">,</span>
<span class="nx">Options</span><span class="p">:</span> <span class="o">&amp;</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">HTMLOptions</span><span class="p">{</span>
<span class="nx">HeaderFilePath</span><span class="p">:</span> <span class="s">&#34;header.html&#34;</span><span class="p">,</span>
<span class="nx">FooterFilePath</span><span class="p">:</span> <span class="s">&#34;footer.html&#34;</span><span class="p">,</span>
<span class="p">},</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="nx">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="nx">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="nx">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="nx">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>
@@ -370,14 +364,12 @@ see to the <a href="#office.fonts">fonts section</a>.</p>
<span class="kd">func</span> <span class="nx">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="o">&amp;</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">HTMLRequest</span><span class="p">{</span>
<span class="nx">IndexFilePath</span><span class="p">:</span> <span class="s">&#34;index.html&#34;</span><span class="p">,</span>
<span class="nx">AssetFilePaths</span><span class="p">:</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span>
<span class="s">&#34;style.css&#34;</span><span class="p">,</span>
<span class="s">&#34;img.png&#34;</span><span class="p">,</span>
<span class="s">&#34;font.woff&#34;</span><span class="p">,</span>
<span class="p">},</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="nx">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="nx">SetAssets</span><span class="p">([]</span><span class="kt">string</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="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="nx">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>
@@ -443,14 +435,10 @@ Also, you have to set both <code>paperWidth</code> and <code>paperHeight</code>.
<span class="kd">func</span> <span class="nx">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="o">&amp;</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">HTMLRequest</span><span class="p">{</span>
<span class="nx">IndexFilePath</span><span class="p">:</span> <span class="s">&#34;index.html&#34;</span><span class="p">,</span>
<span class="nx">Options</span><span class="p">:</span> <span class="o">&amp;</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">HTMLOptions</span><span class="p">{</span>
<span class="nx">PaperSize</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">PaperMargins</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">Landscape</span><span class="p">:</span> <span class="kc">true</span>
<span class="p">},</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="nx">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="nx">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="nx">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="nx">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="nx">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>
@@ -528,12 +516,7 @@ in the file <code>index.html</code>. This function will convert a given markdown
<span class="kd">func</span> <span class="nx">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="o">&amp;</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">MarkdownRequest</span><span class="p">{</span>
<span class="nx">IndexFilePath</span><span class="p">:</span> <span class="s">&#34;index.html&#34;</span><span class="p">,</span>
<span class="nx">MarkdownFilePaths</span><span class="p">:</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span>
<span class="s">&#34;file.md&#34;</span><span class="p">,</span>
<span class="p">},</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="nx">NewMarkdownRequest</span><span class="p">(</span><span class="s">&#34;index.html&#34;</span><span class="p">,</span> <span class="p">[]</span><span class="kt">string</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="nx">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>
@@ -574,6 +557,8 @@ in the file <code>index.html</code>. This function will convert a given markdown
<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>
@@ -613,12 +598,7 @@ See the <a href="#scalability">scalability section</a> to find how to mitigate t
<span class="kd">func</span> <span class="nx">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="o">&amp;</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">OfficeRequest</span><span class="p">{</span>
<span class="nx">FilePaths</span><span class="p">:</span> <span class="p">[]</span><span class="kt">string</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="p">},</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="nx">NewOfficeRequest</span><span class="p">([]</span><span class="kt">string</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="nx">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>
@@ -642,6 +622,68 @@ See the <a href="#scalability">scalability section</a> to find how to mitigate t
</span><span class="x">$filename = $client-&gt;store($request, $dirPath);
</span><span class="x"></span></pre>
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="office.paper_size_and_orientation" href="#office.paper_size_and_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 and orientation</h2>
<p>You may also customize the resulting PDF format.</p>
<p>By default, it will be rendered with <code>A4</code> size and <code>portrait</code> orientation.</p>
<blockquote>
<p>Paper size has to be provided in <code>inches</code>.
Also, you have to set both <code>paperWidth</code> and <code>paperHeight</code>.</p>
</blockquote>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="office.paper_size_and_orientation.c_url" href="#office.paper_size_and_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">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">landscape</span><span class="o">=</span><span class="nb">true</span> <span class="se">\
</span><span class="se"></span> &gt; result.pdf
</pre>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="office.paper_size_and_orientation.go" href="#office.paper_size_and_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="nx">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="nx">NewOfficeRequest</span><span class="p">([]</span><span class="kt">string</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="nx">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="nx">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="nx">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.paper_size_and_orientation.php" href="#office.paper_size_and_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"><span class="x">use TheCodingMachine\Gotenberg\Client;
</span><span class="x">use TheCodingMachine\Gotenberg\DocumentFactory;
</span><span class="x">use TheCodingMachine\Gotenberg\OfficeRequest;
</span><span class="x">
</span><span class="x">$client = new Client(&#39;http://localhost:3000&#39;, new \Http\Adapter\Guzzle6\Client());
</span><span class="x">$files = [
</span><span class="x"> DocumentFactory::makeFromPath(&#39;document.docx&#39;, &#39;document.docx&#39;),
</span><span class="x">];
</span><span class="x">$request = new OfficeRequest($files);
</span><span class="x">$request-&gt;setPaperSize(Request::A4);
</span><span class="x">$request-&gt;setLandscape(true);
</span><span class="x">$dirPath = &#34;/foo&#34;;
</span><span class="x">$filename = $client-&gt;store($request, $dirPath);
</span><span class="x"></span></pre>
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="office.fonts" href="#office.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</h2>
@@ -692,12 +734,7 @@ will merge them and return the resulting PDF file.</p>
<span class="kd">func</span> <span class="nx">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="o">&amp;</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">MergeRequest</span><span class="p">{</span>
<span class="nx">FilePaths</span><span class="p">:</span> <span class="p">[]</span><span class="kt">string</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="p">},</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="nx">NewMergeRequest</span><span class="p">([]</span><span class="kt">string</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="nx">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>
@@ -757,14 +794,10 @@ to given URL.</p>
<span class="kd">func</span> <span class="nx">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="o">&amp;</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">HTMLRequest</span><span class="p">{</span>
<span class="nx">IndexFilePath</span><span class="p">:</span> <span class="s">&#34;index.html&#34;</span><span class="p">,</span>
<span class="nx">Options</span><span class="p">:</span> <span class="o">&amp;</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">HTMLOptions</span><span class="p">{</span>
<span class="nx">WebHookURL</span><span class="p">:</span> <span class="s">&#34;http://myapp.com/webhook/&#34;</span><span class="p">,</span>
<span class="p">},</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="nx">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="nx">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">err</span> <span class="o">:=</span> <span class="nx">c</span><span class="p">.</span><span class="nx">Post</span><span class="p">(</span><span class="nx">req</span><span class="p">)</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="nx">Post</span><span class="p">(</span><span class="nx">req</span><span class="p">)</span>
<span class="p">}</span>
</pre>

2
go.mod
View File

@@ -4,7 +4,7 @@ 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.18
github.com/hhrutter/pdfcpu v0.1.19
github.com/labstack/echo v0.0.0-20181123063703-c7eb8da9ec73
github.com/labstack/gommon v0.2.8
github.com/mafredri/cdp v0.20.0

2
go.sum
View File

@@ -10,6 +10,8 @@ github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH
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 v3.3.5+incompatible h1:9PfxPUmasKzeJor9uQTaXLT6WUG/r+vSTmvXxvv3JO4=

View File

@@ -87,14 +87,15 @@ func newContext(r *resource) (context.Context, context.CancelFunc) {
func print(c echo.Context, p printer.Printer, r *resource) error {
baseFilename, err := rand.Get()
if err != nil {
return fmt.Errorf("getting result file name: %v", err)
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 and error.
// or an error.
if err := p.Print(fpath); err != nil {
return err
}
@@ -104,6 +105,7 @@ func print(c echo.Context, p printer.Printer, r *resource) error {
// 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
@@ -123,3 +125,10 @@ func print(c echo.Context, p printer.Printer, r *resource) error {
}()
return nil
}
func hijackErr(err error, r *resource) error {
if r != nil {
defer r.removeAll()
}
return err
}

View File

@@ -8,9 +8,8 @@ import (
func convertHTML(c echo.Context) error {
r, err := newResource(c)
if err != nil {
return err
return hijackErr(err, r)
}
defer r.removeAll()
ctx, cancel := newContext(r)
if cancel != nil {
defer cancel()
@@ -18,26 +17,26 @@ func convertHTML(c echo.Context) error {
p := &printer.HTML{Context: ctx}
indexPath, err := r.filePath("index.html")
if err != nil {
return err
return hijackErr(err, r)
}
p.WithLocalURL(indexPath)
headerPath, _ := r.filePath("header.html")
if err := p.WithHeaderFile(headerPath); err != nil {
return err
return hijackErr(err, r)
}
footerPath, _ := r.filePath("footer.html")
if err := p.WithFooterFile(footerPath); err != nil {
return err
return hijackErr(err, r)
}
paperSize, err := r.paperSize()
if err != nil {
return err
return hijackErr(err, r)
}
p.PaperWidth = paperSize[0]
p.PaperHeight = paperSize[1]
paperMargins, err := r.paperMargins()
if err != nil {
return err
return hijackErr(err, r)
}
p.MarginTop = paperMargins[0]
p.MarginBottom = paperMargins[1]
@@ -45,7 +44,7 @@ func convertHTML(c echo.Context) error {
p.MarginRight = paperMargins[3]
landscape, err := r.landscape()
if err != nil {
return err
return hijackErr(err, r)
}
p.Landscape = landscape
return print(c, p, r)

View File

@@ -8,35 +8,34 @@ import (
func convertMarkdown(c echo.Context) error {
r, err := newResource(c)
if err != nil {
return err
return hijackErr(err, r)
}
defer r.removeAll()
ctx, cancel := newContext(r)
if cancel != nil {
defer cancel()
}
indexPath, err := r.filePath("index.html")
if err != nil {
return err
return hijackErr(err, r)
}
p := &printer.Markdown{Context: ctx, TemplatePath: indexPath}
headerPath, _ := r.filePath("header.html")
if err := p.WithHeaderFile(headerPath); err != nil {
return err
return hijackErr(err, r)
}
footerPath, _ := r.filePath("footer.html")
if err := p.WithFooterFile(footerPath); err != nil {
return err
return hijackErr(err, r)
}
paperSize, err := r.paperSize()
if err != nil {
return err
return hijackErr(err, r)
}
p.PaperWidth = paperSize[0]
p.PaperHeight = paperSize[1]
paperMargins, err := r.paperMargins()
if err != nil {
return err
return hijackErr(err, r)
}
p.MarginTop = paperMargins[0]
p.MarginBottom = paperMargins[1]
@@ -44,7 +43,7 @@ func convertMarkdown(c echo.Context) error {
p.MarginRight = paperMargins[3]
landscape, err := r.landscape()
if err != nil {
return err
return hijackErr(err, r)
}
p.Landscape = landscape
return print(c, p, r)

View File

@@ -1,6 +1,7 @@
package api
import (
"errors"
"fmt"
"net/http"
"os"
@@ -13,23 +14,26 @@ import (
func merge(c echo.Context) error {
r, err := newResource(c)
if err != nil {
return err
return hijackErr(err, r)
}
defer r.removeAll()
fpaths, err := r.filePaths([]string{".pdf"})
if err != nil {
return err
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 fmt.Errorf("getting result file name: %v", err)
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 and error.
// or an error.
if err := printer.Merge(fpaths, fpath); err != nil {
return err
}
@@ -39,6 +43,7 @@ func merge(c echo.Context) error {
// 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

View File

@@ -8,6 +8,8 @@ import (
)
var officeExts = []string{
".txt",
".rtf",
".doc",
".docx",
".odt",
@@ -22,20 +24,30 @@ var officeExts = []string{
func convertOffice(c echo.Context) error {
r, err := newResource(c)
if err != nil {
return err
return hijackErr(err, r)
}
defer r.removeAll()
ctx, cancel := newContext(r)
if cancel != nil {
defer cancel()
}
fpaths, err := r.filePaths(officeExts)
if err != nil {
return err
return hijackErr(err, r)
}
if len(fpaths) == 0 {
return errors.New("no suitable office documents to convert")
return hijackErr(errors.New("no suitable office documents to convert"), r)
}
p := &printer.Office{Context: ctx, FilePaths: fpaths}
paperSize, err := r.paperSize()
if err != nil {
return hijackErr(err, r)
}
p.PaperWidth = paperSize[0]
p.PaperHeight = paperSize[1]
landscape, err := r.landscape()
if err != nil {
return hijackErr(err, r)
}
p.Landscape = landscape
return print(c, p, r)
}

View File

@@ -49,17 +49,17 @@ func newResource(c echo.Context) (*resource, error) {
r := &resource{values: v, dirPath: dirPath}
form, err := c.MultipartForm()
if err != nil {
return nil, fmt.Errorf("getting multipart form: %v", err)
return r, fmt.Errorf("getting multipart form: %v", err)
}
for _, files := range form.File {
for _, fh := range files {
in, err := fh.Open()
if err != nil {
return nil, fmt.Errorf("%s: opening file: %v", fh.Filename, err)
return r, fmt.Errorf("%s: opening file: %v", fh.Filename, err)
}
defer in.Close()
if err := r.writeFile(fh.Filename, in); err != nil {
return nil, err
return r, err
}
}
}

View File

@@ -1,52 +1,7 @@
/*
Package printer contains structs which convert
a specific file type to PDF:
a specific file type to PDF.
// converting HTML to PDF.
p := &printer.HTML{
Context: context.Background(),
PaperWidth: 8.27,
PaperHeight: 11.7,
Landscape: false,
}
p.WithLocalURL("index.html")
if err := p.Print("result.pdf"); err != nil {
return err
}
// converting Markdown to PDF:
// it assumes here that our template "index.html"
// will call toHTML method to convert
// markdown files to HTML.
p := &printer.Markdown{
Context: context.Background(),
TemplatePath: "index.html",
PaperWidth: 8.27,
PaperHeight: 11.7,
Landscape: false,
}
if err := p.Print("result.pdf"); err != nil {
return err
}
// converting Office documents to PDF:
// it converts each files independently and
// then merge them.
//
// Also, as unoconv cannot perform
// concurrent conversions, a lock is applied.
p := &printer.Office{
Context: ctx,
FilePaths: []string{"document.docx", "presentation.pptx"}
}
if err := p.Print("result.pdf"); err != nil {
return err
}
It is also able to merge a list of PDF files:
if err := printer.Merge([]string{"foo.pdf", "bar.pdf"}, "result.pdf"); err != nil {
return err
}
It is also able to merge a list of PDF files.
*/
package printer

View File

@@ -7,6 +7,7 @@ import (
"os"
"os/exec"
"path/filepath"
"strconv"
"sync"
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
@@ -16,8 +17,11 @@ var mu sync.Mutex
// Office facilitates Office documents to PDF conversion.
type Office struct {
Context context.Context
FilePaths []string
Context context.Context
FilePaths []string
PaperWidth float64
PaperHeight float64
Landscape bool
}
// Print converts Office documents to PDF.
@@ -32,14 +36,24 @@ func (o *Office) Print(destination string) error {
return err
}
tmpDest := fmt.Sprintf("%s/%s.pdf", dirPath, baseFilename)
paperSize, err := unoconvPaperSize(o.PaperWidth, o.PaperHeight)
if err != nil {
return err
}
cmdArgs := []string{
"--format",
"pdf",
"--printer",
paperSize,
}
if o.Landscape {
cmdArgs = append(cmdArgs, "--printer", "PaperOrientation=landscape")
}
cmdArgs = append(cmdArgs, "--output", tmpDest, fpath)
cmd := exec.CommandContext(
o.Context,
"unoconv",
"--format",
"pdf",
"--output",
tmpDest,
fpath,
cmdArgs...,
)
_, err = cmd.Output()
if o.Context.Err() == context.DeadlineExceeded {
@@ -56,6 +70,18 @@ func (o *Office) Print(destination string) error {
return Merge(fpaths, destination)
}
func unoconvPaperSize(paperWidth, paperHeight float64) (string, error) {
width, err := strconv.Atoi(fmt.Sprintf("%.0f", paperWidth*25.4))
if err != nil {
return "", fmt.Errorf("%0.f: converting width to millimiter: %v", paperWidth, err)
}
height, err := strconv.Atoi(fmt.Sprintf("%.0f", paperHeight*25.4))
if err != nil {
return "", fmt.Errorf("%0.f: converting height to millimiter: %v", paperHeight, err)
}
return fmt.Sprintf("PaperSize=%dx%d", width, height), nil
}
// Compile-time checks to ensure type implements desired interfaces.
var (
_ = Printer(new(Office))

View File

@@ -20,6 +20,8 @@ func TestOffice(t *testing.T) {
Context: ctx,
FilePaths: []string{
fmt.Sprintf("%s/%s", dirPath, "document.docx"),
fmt.Sprintf("%s/%s", dirPath, "document.txt"),
fmt.Sprintf("%s/%s", dirPath, "document.rtf"),
},
}
dst := fmt.Sprintf("%s/%s", dirPath, "foo.pdf")

View File

@@ -5,9 +5,16 @@ import (
"io/ioutil"
pdfcpuAPI "github.com/hhrutter/pdfcpu/pkg/api"
pdfcpuLog "github.com/hhrutter/pdfcpu/pkg/log"
pdfcpuConfig "github.com/hhrutter/pdfcpu/pkg/pdfcpu"
)
func init() {
// disable loggers when merging
// PDFs.
pdfcpuLog.DisableLoggers()
}
// Printer is a type that can create a PDF file from a source.
// The source is defined in the underlying implementation.
type Printer interface {

View File

@@ -16,7 +16,7 @@ func TestMerge(t *testing.T) {
err := Merge(
[]string{
fmt.Sprintf("%s/%s", dirPath, "gotenberg.pdf"),
fmt.Sprintf("%s/%s", dirPath, "gotenberg.pdf"),
fmt.Sprintf("%s/%s", dirPath, "gotenberg_bis.pdf"),
},
dst,
)

View File

@@ -16,19 +16,17 @@ import "github.com/thecodingmachine/gotenberg/pkg"
func main() {
// HTML conversion example.
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req := &gotenberg.HTMLRequest{
IndexFilePath: "index.html",
AssetFilePaths: []string{
"style.css",
"img.png",
},
Options: &gotenberg.HTMLOptions{
HeaderFilePath: "header.html",
FooterFilePath: "footer.html",
PaperSize: gotenberg.A4,
PaperMargins: gotenberg.NormalMargins,
},
}
req, _ := gotenberg.NewHTMLRequest("index.html")
req.SetHeader("header.html")
req.SetFooter("footer.html")
req.SetAssets([]string{
"font.woff",
"img.gif",
"style.css",
})
req.SetPaperSize(gotenberg.A4)
req.SetMargins(gotenberg.NormalMargins)
req.SetLandscape(false)
dest := "foo.pdf"
c.Store(req, dest)
}

View File

@@ -59,18 +59,35 @@ type Client struct {
// form values and form files to
// the Gotenberg API.
type Request interface {
validate() error
SetWebhookURL(webhookURL string)
getPostURL() string
getFormValues() map[string]string
getFormFiles() map[string]string
}
// ChromeRequest is a type for sending
// conversion requests which will be
// handle by Google Chrome.
type ChromeRequest interface {
SetHeader(fpath string) error
SetFooter(fpath string) error
SetAssets(fpaths []string) error
SetPaperSize(size [2]float64)
SetMargins(margins [4]float64)
SetLandscape(isLandscape bool)
}
// UnoconvRequest is a type for sending
// conversion requests which will be
// handle by unoconv.
type UnoconvRequest interface {
SetPaperSize(size [2]float64)
SetLandscape(landscape bool)
}
// Post sends a request to the Gotenberg API
// and returns the response.
func (c *Client) Post(req Request) (*http.Response, error) {
if err := req.validate(); err != nil {
return nil, err
}
body, contentType, err := multipartForm(req)
if err != nil {
return nil, err

View File

@@ -2,30 +2,7 @@
Package gotenberg is a Go client for
interacting with a Gotenberg API.
For instance, if you want to convert HTML:
import "github.com/thecodingmachine/gotenberg/pkg"
func main() {
// HTML conversion example.
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req := &gotenberg.HTMLRequest{
IndexFilePath: "index.html",
AssetFilePaths: []string{
"style.css",
"img.png",
},
Options: &gotenberg.HTMLOptions{
HeaderFilePath: "header.html",
FooterFilePath: "footer.html",
PaperSize: gotenberg.A4,
PaperMargins: gotenberg.NormalMargins,
},
}
dest := "foo.pdf"
c.Store(req, dest)
}
For more complete usages, head to the https://thecodingmachine.gotenberg.github.io.
For more complete usages, head to the documentation:
https://thecodingmachine.gotenberg.github.io.
*/
package gotenberg

View File

@@ -9,64 +9,89 @@ import (
// HTMLRequest facilitates HTML conversion
// with the Gotenberg API.
type HTMLRequest struct {
IndexFilePath string
AssetFilePaths []string
Options *HTMLOptions
indexFilePath string
headerFilePath string
footerFilePath string
assetFilePaths []string
values map[string]string
}
// HTMLOptions gathers all options
// for HTML conversion with the Gotenberg API.
type HTMLOptions struct {
WebHookURL string
HeaderFilePath string
FooterFilePath string
PaperSize [2]float64
PaperMargins [4]float64
Landscape bool
// NewHTMLRequest create HTMLRequest.
func NewHTMLRequest(indexFilePath string) (*HTMLRequest, error) {
if !fileExists(indexFilePath) {
return nil, fmt.Errorf("%s: index file does not exist", indexFilePath)
}
return &HTMLRequest{indexFilePath: indexFilePath, values: make(map[string]string)}, nil
}
func (html *HTMLRequest) validate() error {
if !fileExists(html.IndexFilePath) {
return fmt.Errorf("%s: index file does not exist", html.IndexFilePath)
}
if html.Options.HeaderFilePath != "" && !fileExists(html.Options.HeaderFilePath) {
return fmt.Errorf("%s: header file does not exist", html.Options.HeaderFilePath)
}
if html.Options.FooterFilePath != "" && !fileExists(html.Options.FooterFilePath) {
return fmt.Errorf("%s: footer file does not exist", html.Options.FooterFilePath)
// SetWebhookURL sets webhookURL form field.
func (html *HTMLRequest) SetWebhookURL(webhookURL string) {
html.values[webhookURL] = webhookURL
}
// SetHeader sets header form file.
func (html *HTMLRequest) SetHeader(fpath string) error {
if !fileExists(fpath) {
return fmt.Errorf("%s: header file does not exist", fpath)
}
html.headerFilePath = fpath
return nil
}
// SetFooter sets footer form file.
func (html *HTMLRequest) SetFooter(fpath string) error {
if !fileExists(fpath) {
return fmt.Errorf("%s: footer file does not exist", fpath)
}
html.footerFilePath = fpath
return nil
}
// SetAssets sets assets form files.
func (html *HTMLRequest) SetAssets(fpaths []string) error {
for _, fpath := range fpaths {
if !fileExists(fpath) {
return fmt.Errorf("%s: file does not exist", fpath)
}
}
html.assetFilePaths = fpaths
return nil
}
// SetPaperSize sets paperWidth and paperHeight form fields.
func (html *HTMLRequest) SetPaperSize(size [2]float64) {
html.values[paperWidth] = fmt.Sprintf("%f", size[0])
html.values[paperHeight] = fmt.Sprintf("%f", size[1])
}
// SetMargins sets marginTop, marginBottom,
// marginLeft and marginRight form fields.
func (html *HTMLRequest) SetMargins(margins [4]float64) {
html.values[marginTop] = fmt.Sprintf("%f", margins[0])
html.values[marginBottom] = fmt.Sprintf("%f", margins[1])
html.values[marginLeft] = fmt.Sprintf("%f", margins[2])
html.values[marginRight] = fmt.Sprintf("%f", margins[3])
}
// SetLandscape sets landscape form field.
func (html *HTMLRequest) SetLandscape(isLandscape bool) {
html.values[landscape] = strconv.FormatBool(isLandscape)
}
func (html *HTMLRequest) getPostURL() string {
return "/convert/html"
}
func (html *HTMLRequest) getFormValues() map[string]string {
if html.Options == nil {
html.Options = &HTMLOptions{}
}
values := make(map[string]string)
values[webhookURL] = html.Options.WebHookURL
values[paperWidth] = fmt.Sprintf("%f", html.Options.PaperSize[0])
values[paperHeight] = fmt.Sprintf("%f", html.Options.PaperSize[1])
values[marginTop] = fmt.Sprintf("%f", html.Options.PaperMargins[0])
values[marginBottom] = fmt.Sprintf("%f", html.Options.PaperMargins[1])
values[marginLeft] = fmt.Sprintf("%f", html.Options.PaperMargins[2])
values[marginRight] = fmt.Sprintf("%f", html.Options.PaperMargins[3])
values[landscape] = strconv.FormatBool(html.Options.Landscape)
return values
return html.values
}
func (html *HTMLRequest) getFormFiles() map[string]string {
if html.Options == nil {
html.Options = &HTMLOptions{}
}
files := make(map[string]string)
files["index.html"] = html.IndexFilePath
files["header.html"] = html.Options.HeaderFilePath
files["footer.html"] = html.Options.FooterFilePath
for _, fpath := range html.AssetFilePaths {
files["index.html"] = html.indexFilePath
files["header.html"] = html.headerFilePath
files["footer.html"] = html.footerFilePath
for _, fpath := range html.assetFilePaths {
files[filepath.Base(fpath)] = fpath
}
return files
@@ -75,4 +100,5 @@ func (html *HTMLRequest) getFormFiles() map[string]string {
// Compile-time checks to ensure type implements desired interfaces.
var (
_ = Request(new(HTMLRequest))
_ = ChromeRequest(new(HTMLRequest))
)

View File

@@ -13,20 +13,21 @@ import (
func TestHTML(t *testing.T) {
c := &Client{Hostname: "http://localhost:3000"}
req := &HTMLRequest{
IndexFilePath: test.HTMLTestFilePath(t, "index.html"),
AssetFilePaths: []string{
test.HTMLTestFilePath(t, "font.woff"),
test.HTMLTestFilePath(t, "img.gif"),
test.HTMLTestFilePath(t, "style.css"),
},
Options: &HTMLOptions{
HeaderFilePath: test.HTMLTestFilePath(t, "header.html"),
FooterFilePath: test.HTMLTestFilePath(t, "footer.html"),
PaperSize: A4,
PaperMargins: NormalMargins,
},
}
req, err := NewHTMLRequest(test.HTMLTestFilePath(t, "index.html"))
require.Nil(t, err)
err = req.SetHeader(test.HTMLTestFilePath(t, "header.html"))
require.Nil(t, err)
err = req.SetFooter(test.HTMLTestFilePath(t, "footer.html"))
require.Nil(t, err)
err = req.SetAssets([]string{
test.HTMLTestFilePath(t, "font.woff"),
test.HTMLTestFilePath(t, "img.gif"),
test.HTMLTestFilePath(t, "style.css"),
})
require.Nil(t, err)
req.SetPaperSize(A4)
req.SetMargins(NormalMargins)
req.SetLandscape(false)
dirPath, err := rand.Get()
require.Nil(t, err)
dest := fmt.Sprintf("%s/foo.pdf", dirPath)
@@ -36,3 +37,11 @@ func TestHTML(t *testing.T) {
err = os.RemoveAll(dirPath)
assert.Nil(t, err)
}
func TestConcurrentHTML(t *testing.T) {
for i := 0; i < 10; i++ {
go func() {
TestHTML(t)
}()
}
}

View File

@@ -9,73 +9,102 @@ import (
// MarkdownRequest facilitates Markdown conversion
// with the Gotenberg API.
type MarkdownRequest struct {
IndexFilePath string
MarkdownFilePaths []string
AssetFilePaths []string
Options *MarkdownOptions
indexFilePath string
markdownFilePaths []string
headerFilePath string
footerFilePath string
assetFilePaths []string
values map[string]string
}
// MarkdownOptions gathers all options
// for Markdown conversion with the Gotenberg API.
type MarkdownOptions struct {
WebHookURL string
HeaderFilePath string
FooterFilePath string
PaperSize [2]float64
PaperMargins [4]float64
Landscape bool
}
func (markdown *MarkdownRequest) validate() error {
if !fileExists(markdown.IndexFilePath) {
return fmt.Errorf("%s: index file does not exist", markdown.IndexFilePath)
// NewMarkdownRequest create MarkdownRequest.
func NewMarkdownRequest(indexFilePath string, markdownFilePaths []string) (*MarkdownRequest, error) {
if !fileExists(indexFilePath) {
return nil, fmt.Errorf("%s: index file does not exist", indexFilePath)
}
if markdown.Options.HeaderFilePath != "" && !fileExists(markdown.Options.HeaderFilePath) {
return fmt.Errorf("%s: header file does not exist", markdown.Options.HeaderFilePath)
}
if markdown.Options.FooterFilePath != "" && !fileExists(markdown.Options.FooterFilePath) {
return fmt.Errorf("%s: footer file does not exist", markdown.Options.FooterFilePath)
}
for _, fpath := range markdown.MarkdownFilePaths {
for _, fpath := range markdownFilePaths {
if !fileExists(fpath) {
return fmt.Errorf("%s: markdown file does not exist", fpath)
return nil, fmt.Errorf("%s: markdown file does not exist", fpath)
}
}
return &MarkdownRequest{
indexFilePath: indexFilePath,
markdownFilePaths: markdownFilePaths,
values: make(map[string]string),
}, nil
}
// SetWebhookURL sets webhookURL form field.
func (markdown *MarkdownRequest) SetWebhookURL(webhookURL string) {
markdown.values[webhookURL] = webhookURL
}
// SetHeader sets header form file.
func (markdown *MarkdownRequest) SetHeader(fpath string) error {
if !fileExists(fpath) {
return fmt.Errorf("%s: header file does not exist", fpath)
}
markdown.headerFilePath = fpath
return nil
}
// SetFooter sets footer form file.
func (markdown *MarkdownRequest) SetFooter(fpath string) error {
if !fileExists(fpath) {
return fmt.Errorf("%s: footer file does not exist", fpath)
}
markdown.footerFilePath = fpath
return nil
}
// SetAssets sets assets form files.
func (markdown *MarkdownRequest) SetAssets(fpaths []string) error {
for _, fpath := range fpaths {
if !fileExists(fpath) {
return fmt.Errorf("%s: file does not exist", fpath)
}
}
markdown.assetFilePaths = fpaths
return nil
}
// SetPaperSize sets paperWidth and paperHeight form fields.
func (markdown *MarkdownRequest) SetPaperSize(size [2]float64) {
markdown.values[paperWidth] = fmt.Sprintf("%f", size[0])
markdown.values[paperHeight] = fmt.Sprintf("%f", size[1])
}
// SetMargins sets marginTop, marginBottom,
// marginLeft and marginRight form fields.
func (markdown *MarkdownRequest) SetMargins(margins [4]float64) {
markdown.values[marginTop] = fmt.Sprintf("%f", margins[0])
markdown.values[marginBottom] = fmt.Sprintf("%f", margins[1])
markdown.values[marginLeft] = fmt.Sprintf("%f", margins[2])
markdown.values[marginRight] = fmt.Sprintf("%f", margins[3])
}
// SetLandscape sets landscape form field.
func (markdown *MarkdownRequest) SetLandscape(isLandscape bool) {
markdown.values[landscape] = strconv.FormatBool(isLandscape)
}
func (markdown *MarkdownRequest) getPostURL() string {
return "/convert/markdown"
}
func (markdown *MarkdownRequest) getFormValues() map[string]string {
if markdown.Options == nil {
markdown.Options = &MarkdownOptions{}
}
values := make(map[string]string)
values[webhookURL] = markdown.Options.WebHookURL
values[paperWidth] = fmt.Sprintf("%f", markdown.Options.PaperSize[0])
values[paperHeight] = fmt.Sprintf("%f", markdown.Options.PaperSize[1])
values[marginTop] = fmt.Sprintf("%f", markdown.Options.PaperMargins[0])
values[marginBottom] = fmt.Sprintf("%f", markdown.Options.PaperMargins[1])
values[marginLeft] = fmt.Sprintf("%f", markdown.Options.PaperMargins[2])
values[marginRight] = fmt.Sprintf("%f", markdown.Options.PaperMargins[3])
values[landscape] = strconv.FormatBool(markdown.Options.Landscape)
return values
return markdown.values
}
func (markdown *MarkdownRequest) getFormFiles() map[string]string {
if markdown.Options == nil {
markdown.Options = &MarkdownOptions{}
}
files := make(map[string]string)
files["index.html"] = markdown.IndexFilePath
files["header.html"] = markdown.Options.HeaderFilePath
files["footer.html"] = markdown.Options.FooterFilePath
for _, fpath := range markdown.MarkdownFilePaths {
files["index.html"] = markdown.indexFilePath
files["header.html"] = markdown.headerFilePath
files["footer.html"] = markdown.footerFilePath
for _, fpath := range markdown.markdownFilePaths {
files[filepath.Base(fpath)] = fpath
}
for _, fpath := range markdown.AssetFilePaths {
for _, fpath := range markdown.assetFilePaths {
files[filepath.Base(fpath)] = fpath
}
return files
@@ -84,4 +113,5 @@ func (markdown *MarkdownRequest) getFormFiles() map[string]string {
// Compile-time checks to ensure type implements desired interfaces.
var (
_ = Request(new(MarkdownRequest))
_ = ChromeRequest(new(MarkdownRequest))
)

View File

@@ -13,25 +13,28 @@ import (
func TestMarkdown(t *testing.T) {
c := &Client{Hostname: "http://localhost:3000"}
req := &MarkdownRequest{
IndexFilePath: test.MarkdownTestFilePath(t, "index.html"),
MarkdownFilePaths: []string{
req, err := NewMarkdownRequest(
test.MarkdownTestFilePath(t, "index.html"),
[]string{
test.MarkdownTestFilePath(t, "paragraph1.md"),
test.MarkdownTestFilePath(t, "paragraph2.md"),
test.MarkdownTestFilePath(t, "paragraph3.md"),
},
AssetFilePaths: []string{
test.HTMLTestFilePath(t, "font.woff"),
test.HTMLTestFilePath(t, "img.gif"),
test.HTMLTestFilePath(t, "style.css"),
},
Options: &MarkdownOptions{
HeaderFilePath: test.MarkdownTestFilePath(t, "header.html"),
FooterFilePath: test.MarkdownTestFilePath(t, "footer.html"),
PaperSize: A4,
PaperMargins: NormalMargins,
},
}
)
require.Nil(t, err)
err = req.SetHeader(test.MarkdownTestFilePath(t, "header.html"))
require.Nil(t, err)
err = req.SetFooter(test.MarkdownTestFilePath(t, "footer.html"))
require.Nil(t, err)
err = req.SetAssets([]string{
test.MarkdownTestFilePath(t, "font.woff"),
test.MarkdownTestFilePath(t, "img.gif"),
test.MarkdownTestFilePath(t, "style.css"),
})
require.Nil(t, err)
req.SetPaperSize(A4)
req.SetMargins(NormalMargins)
req.SetLandscape(false)
dirPath, err := rand.Get()
require.Nil(t, err)
dest := fmt.Sprintf("%s/foo.pdf", dirPath)
@@ -41,3 +44,11 @@ func TestMarkdown(t *testing.T) {
err = os.RemoveAll(dirPath)
assert.Nil(t, err)
}
func TestConcurrentMarkdown(t *testing.T) {
for i := 0; i < 10; i++ {
go func() {
TestMarkdown(t)
}()
}
}

View File

@@ -8,24 +8,23 @@ import (
// MergeRequest facilitates merging PDF
// with the Gotenberg API.
type MergeRequest struct {
FilePaths []string
Options *MergeOptions
filePaths []string
values map[string]string
}
// MergeOptions gathers all options
// for merging PDF
// with the Gotenberg API.
type MergeOptions struct {
WebHookURL string
}
func (merge *MergeRequest) validate() error {
for _, fpath := range merge.FilePaths {
// NewMergeRequest create MergeRequest.
func NewMergeRequest(fpaths []string) (*MergeRequest, error) {
for _, fpath := range fpaths {
if !fileExists(fpath) {
return fmt.Errorf("%s: PDF file does not exist", fpath)
return nil, fmt.Errorf("%s: file does not exist", fpath)
}
}
return nil
return &MergeRequest{filePaths: fpaths, values: make(map[string]string)}, nil
}
// SetWebhookURL sets webhookURL form field.
func (merge *MergeRequest) SetWebhookURL(webhookURL string) {
merge.values[webhookURL] = webhookURL
}
func (merge *MergeRequest) getPostURL() string {
@@ -33,17 +32,12 @@ func (merge *MergeRequest) getPostURL() string {
}
func (merge *MergeRequest) getFormValues() map[string]string {
if merge.Options == nil {
merge.Options = &MergeOptions{}
}
values := make(map[string]string)
values[webhookURL] = merge.Options.WebHookURL
return values
return merge.values
}
func (merge *MergeRequest) getFormFiles() map[string]string {
files := make(map[string]string)
for _, fpath := range merge.FilePaths {
for _, fpath := range merge.filePaths {
files[filepath.Base(fpath)] = fpath
}
return files

View File

@@ -13,12 +13,11 @@ import (
func TestMerge(t *testing.T) {
c := &Client{Hostname: "http://localhost:3000"}
req := &MergeRequest{
FilePaths: []string{
test.PDFTestFilePath(t, "gotenberg.pdf"),
test.PDFTestFilePath(t, "gotenberg.pdf"),
},
}
req, err := NewMergeRequest([]string{
test.PDFTestFilePath(t, "gotenberg.pdf"),
test.PDFTestFilePath(t, "gotenberg.pdf"),
})
require.Nil(t, err)
dirPath, err := rand.Get()
require.Nil(t, err)
dest := fmt.Sprintf("%s/foo.pdf", dirPath)
@@ -28,3 +27,11 @@ func TestMerge(t *testing.T) {
err = os.RemoveAll(dirPath)
assert.Nil(t, err)
}
func TestConcurrentMerge(t *testing.T) {
for i := 0; i < 10; i++ {
go func() {
TestMerge(t)
}()
}
}

View File

@@ -3,29 +3,40 @@ package gotenberg
import (
"fmt"
"path/filepath"
"strconv"
)
// OfficeRequest facilitates Office documents
// conversion with the Gotenberg API.
type OfficeRequest struct {
FilePaths []string
Options *OfficeOptions
filePaths []string
values map[string]string
}
// OfficeOptions gathers all options
// for Office documents conversion
// with the Gotenberg API.
type OfficeOptions struct {
WebHookURL string
}
func (office *OfficeRequest) validate() error {
for _, fpath := range office.FilePaths {
// NewOfficeRequest create OfficeRequest.
func NewOfficeRequest(fpaths []string) (*OfficeRequest, error) {
for _, fpath := range fpaths {
if !fileExists(fpath) {
return fmt.Errorf("%s: office file does not exist", fpath)
return nil, fmt.Errorf("%s: file does not exist", fpath)
}
}
return nil
return &OfficeRequest{filePaths: fpaths, values: make(map[string]string)}, nil
}
// SetWebhookURL sets webhookURL form field.
func (office *OfficeRequest) SetWebhookURL(webhookURL string) {
office.values[webhookURL] = webhookURL
}
// SetPaperSize sets paperWidth and paperHeight form fields.
func (office *OfficeRequest) SetPaperSize(size [2]float64) {
office.values[paperWidth] = fmt.Sprintf("%f", size[0])
office.values[paperHeight] = fmt.Sprintf("%f", size[1])
}
// SetLandscape sets landscape form field.
func (office *OfficeRequest) SetLandscape(isLandscape bool) {
office.values[landscape] = strconv.FormatBool(isLandscape)
}
func (office *OfficeRequest) getPostURL() string {
@@ -33,17 +44,12 @@ func (office *OfficeRequest) getPostURL() string {
}
func (office *OfficeRequest) getFormValues() map[string]string {
if office.Options == nil {
office.Options = &OfficeOptions{}
}
values := make(map[string]string)
values[webhookURL] = office.Options.WebHookURL
return values
return office.values
}
func (office *OfficeRequest) getFormFiles() map[string]string {
files := make(map[string]string)
for _, fpath := range office.FilePaths {
for _, fpath := range office.filePaths {
files[filepath.Base(fpath)] = fpath
}
return files
@@ -52,4 +58,5 @@ func (office *OfficeRequest) getFormFiles() map[string]string {
// Compile-time checks to ensure type implements desired interfaces.
var (
_ = Request(new(OfficeRequest))
_ = UnoconvRequest(new(OfficeRequest))
)

View File

@@ -13,11 +13,12 @@ import (
func TestOffice(t *testing.T) {
c := &Client{Hostname: "http://localhost:3000"}
req := &OfficeRequest{
FilePaths: []string{
test.OfficeTestFilePath(t, "document.docx"),
},
}
req, err := NewOfficeRequest([]string{
test.OfficeTestFilePath(t, "document.docx"),
})
require.Nil(t, err)
req.SetPaperSize(A4)
req.SetLandscape(false)
dirPath, err := rand.Get()
require.Nil(t, err)
dest := fmt.Sprintf("%s/foo.pdf", dirPath)
@@ -27,3 +28,11 @@ func TestOffice(t *testing.T) {
err = os.RemoveAll(dirPath)
assert.Nil(t, err)
}
func TestConcurrentOffice(t *testing.T) {
for i := 0; i < 10; i++ {
go func() {
TestOffice(t)
}()
}
}

2196
test/testdata/office/document.rtf vendored Normal file

File diff suppressed because it is too large Load Diff

3
test/testdata/office/document.txt vendored Normal file
View File

@@ -0,0 +1,3 @@
Gutenberg
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.

BIN
test/testdata/pdf/gotenberg_bis.pdf vendored Normal file

Binary file not shown.