mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-14 19:32:15 +01:00
feat(pdfengines): optimize PDF images to reduce file size (#359)
This commit is contained in:
@@ -27,7 +27,7 @@ Available tags:
|
||||
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Chromium | `chromium`, `chromium-concurrent`, `chromium-convert-html`, `chromium-convert-markdown`, `chromium-convert-url`, `chromium-screenshot-html`, `chromium-screenshot-markdown`, `chromium-screenshot-url`, `chromium-ssrf` |
|
||||
| LibreOffice | `libreoffice`, `libreoffice-convert`, `libreoffice-ssrf` |
|
||||
| PDF Engines | `pdfengines`, `pdfengines-convert`, `pdfengines-merge`, `merge`, `pdfengines-split`, `split`, `pdfengines-flatten`, `flatten`, `pdfengines-rotate`, `rotate`, `pdfengines-embed`, `embed`, `pdfengines-encrypt`, `encrypt`, `pdfengines-watermark`, `watermark`, `pdfengines-stamp`, `stamp`, `pdfengines-metadata`, `metadata`, `pdfengines-bookmarks`, `bookmarks` |
|
||||
| PDF Engines | `pdfengines`, `pdfengines-convert`, `pdfengines-merge`, `merge`, `pdfengines-split`, `split`, `pdfengines-flatten`, `flatten`, `pdfengines-optimize`, `optimize`, `pdfengines-rotate`, `rotate`, `pdfengines-embed`, `embed`, `pdfengines-encrypt`, `encrypt`, `pdfengines-watermark`, `watermark`, `pdfengines-stamp`, `stamp`, `pdfengines-metadata`, `metadata`, `pdfengines-bookmarks`, `bookmarks` |
|
||||
| Infra | `health`, `debug`, `root`, `version`, `output-filename`, `prometheus-metrics`, `webhook`, `download-from` |
|
||||
|
||||
## Writing a new test
|
||||
|
||||
@@ -1187,6 +1187,26 @@ Feature: /forms/chromium/convert/html
|
||||
Then there should be 1 PDF(s) in the response
|
||||
Then the response PDF(s) should be flatten
|
||||
|
||||
# Post-processing image optimization re-encodes the embedded lossless image to
|
||||
# JPEG. The same page is ~700 KB without it and well under 300 KB with it.
|
||||
# See https://github.com/gotenberg/gotenberg/issues/359.
|
||||
Scenario: POST /forms/chromium/convert/html (Optimize Images)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
|
||||
| files | testdata/optimize-image-html/index.html | file |
|
||||
| files | testdata/optimize-image-html/image.png | file |
|
||||
| Gotenberg-Output-Filename | foo | header |
|
||||
Then the response status code should be 200
|
||||
Then the "foo.pdf" file size should be greater than 300 KB
|
||||
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
|
||||
| files | testdata/optimize-image-html/index.html | file |
|
||||
| files | testdata/optimize-image-html/image.png | file |
|
||||
| optimizeImages | true | field |
|
||||
| Gotenberg-Output-Filename | foo | header |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
Then the "foo.pdf" file size should be less than 300 KB
|
||||
|
||||
@encrypt
|
||||
Scenario: POST /forms/chromium/convert/html (Encrypt - user password only)
|
||||
Given I have a default Gotenberg container
|
||||
|
||||
61
test/integration/features/pdfengines_optimize.feature
Normal file
61
test/integration/features/pdfengines_optimize.feature
Normal file
@@ -0,0 +1,61 @@
|
||||
@pdfengines
|
||||
@pdfengines-optimize
|
||||
@optimize
|
||||
Feature: /forms/pdfengines/optimize
|
||||
|
||||
# image-heavy.pdf is a ~710 KB PDF whose single image Chromium embedded
|
||||
# losslessly (FlateDecode). Re-encoding it to JPEG shrinks the file well
|
||||
# below this threshold while leaving the structure intact.
|
||||
# See https://github.com/gotenberg/gotenberg/issues/359.
|
||||
Scenario: POST /forms/pdfengines/optimize (Image-heavy PDF)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/optimize" endpoint with the following form data and header(s):
|
||||
| files | testdata/image-heavy.pdf | file |
|
||||
| Gotenberg-Output-Filename | foo | header |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
Then there should be 1 PDF(s) in the response
|
||||
Then the "foo.pdf" file size should be less than 300 KB
|
||||
|
||||
Scenario: POST /forms/pdfengines/optimize (Custom Image Quality)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/optimize" endpoint with the following form data and header(s):
|
||||
| files | testdata/image-heavy.pdf | file |
|
||||
| imageQuality | 40 | field |
|
||||
| Gotenberg-Output-Filename | foo | header |
|
||||
Then the response status code should be 200
|
||||
Then the "foo.pdf" file size should be less than 300 KB
|
||||
|
||||
Scenario: POST /forms/pdfengines/optimize (PDF Without Images)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/optimize" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
Then there should be 1 PDF(s) in the response
|
||||
|
||||
Scenario: POST /forms/pdfengines/optimize (Invalid Image Quality)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/optimize" endpoint with the following form data and header(s):
|
||||
| files | testdata/image-heavy.pdf | file |
|
||||
| imageQuality | 200 | field |
|
||||
Then the response status code should be 400
|
||||
Then the response header "Content-Type" should be "text/plain; charset=UTF-8"
|
||||
|
||||
Scenario: POST /forms/pdfengines/optimize (Bad Request)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/optimize" endpoint with the following form data and header(s):
|
||||
| Gotenberg-Output-Filename | foo | header |
|
||||
Then the response status code should be 400
|
||||
Then the response header "Content-Type" should be "text/plain; charset=UTF-8"
|
||||
Then the response body should match string:
|
||||
"""
|
||||
Invalid form data: no form file found for extensions: [.pdf]
|
||||
"""
|
||||
|
||||
Scenario: POST /forms/pdfengines/optimize (Routes Disabled)
|
||||
Given I have a Gotenberg container with the following environment variable(s):
|
||||
| PDFENGINES_DISABLE_ROUTES | true |
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/optimize" endpoint with the following form data and header(s):
|
||||
| files | testdata/image-heavy.pdf | file |
|
||||
Then the response status code should be 404
|
||||
@@ -1057,6 +1057,29 @@ func (s *scenario) theImagePixelShouldBe(_ context.Context, name string, x, y in
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) theFileSizeShouldBe(_ context.Context, name, comparator string, sizeKB int) error {
|
||||
path := fmt.Sprintf("%s/%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"), name)
|
||||
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("stat %q: %w", path, err)
|
||||
}
|
||||
|
||||
limit := int64(sizeKB) * 1024
|
||||
switch comparator {
|
||||
case "less":
|
||||
if info.Size() >= limit {
|
||||
return fmt.Errorf("expected %s (%d bytes) to be smaller than %d KB", name, info.Size(), sizeKB)
|
||||
}
|
||||
case "greater":
|
||||
if info.Size() <= limit {
|
||||
return fmt.Errorf("expected %s (%d bytes) to be larger than %d KB", name, info.Size(), sizeKB)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) thePdfShouldBeSetToLandscapeOrientation(ctx context.Context, name string, kind string) error {
|
||||
var path string
|
||||
if !strings.HasPrefix(name, "*_") {
|
||||
@@ -1650,6 +1673,7 @@ func InitializeScenario(ctx *godog.ScenarioContext) {
|
||||
ctx.Then(`^the "([^"]*)" PDF should have (\d+) image\(s\)$`, s.thePdfShouldHaveImages)
|
||||
ctx.Then(`^the "([^"]*)" image should be (\d+)x(\d+) pixels$`, s.theImageShouldBePixels)
|
||||
ctx.Then(`^the "([^"]*)" image pixel at (\d+),(\d+) should be "([^"]*)"$`, s.theImagePixelShouldBe)
|
||||
ctx.Then(`^the "([^"]*)" file size should be (less|greater) than (\d+) KB$`, s.theFileSizeShouldBe)
|
||||
ctx.After(func(ctx context.Context, sc *godog.Scenario, err error) (context.Context, error) {
|
||||
if s.gotenbergContainer != nil {
|
||||
errTerminate := s.gotenbergContainer.Terminate(ctx, testcontainers.StopTimeout(0))
|
||||
|
||||
BIN
test/integration/testdata/image-heavy.pdf
vendored
Normal file
BIN
test/integration/testdata/image-heavy.pdf
vendored
Normal file
Binary file not shown.
BIN
test/integration/testdata/optimize-image-html/image.png
vendored
Normal file
BIN
test/integration/testdata/optimize-image-html/image.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
15
test/integration/testdata/optimize-image-html/index.html
vendored
Normal file
15
test/integration/testdata/optimize-image-html/index.html
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Image-heavy page</title>
|
||||
<style>
|
||||
@page { margin: 0; }
|
||||
body { margin: 0; }
|
||||
img { width: 100%; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<img src="image.png" />
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user