mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-10-08 05:23:18 +01:00
test(chromium): cover header and footer image deduplication
This commit is contained in:
committed by
Julien Neuhart
parent
cc9cd5d82d
commit
c83dbd7baf
1
Makefile
1
Makefile
@@ -154,6 +154,7 @@ NO_CONCURRENCY=false
|
|||||||
# chromium
|
# chromium
|
||||||
# chromium-concurrent
|
# chromium-concurrent
|
||||||
# chromium-convert-html
|
# chromium-convert-html
|
||||||
|
# chromium-image-deduplication
|
||||||
# chromium-convert-markdown
|
# chromium-convert-markdown
|
||||||
# chromium-convert-url
|
# chromium-convert-url
|
||||||
# chromium-screenshot-html
|
# chromium-screenshot-html
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ Available tags:
|
|||||||
|
|
||||||
| Group | Tags |
|
| Group | 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` |
|
| Chromium | `chromium`, `chromium-concurrent`, `chromium-convert-html`, `chromium-image-deduplication`, `chromium-convert-markdown`, `chromium-convert-url`, `chromium-screenshot-html`, `chromium-screenshot-markdown`, `chromium-screenshot-url`, `chromium-ssrf` |
|
||||||
| LibreOffice | `libreoffice`, `libreoffice-convert`, `libreoffice-ssrf` |
|
| LibreOffice | `libreoffice`, `libreoffice-convert`, `libreoffice-ssrf` |
|
||||||
| 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` |
|
| 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` |
|
| Infra | `health`, `debug`, `root`, `version`, `output-filename`, `prometheus-metrics`, `webhook`, `download-from` |
|
||||||
@@ -75,6 +75,7 @@ Available tags:
|
|||||||
- `the (response|webhook request) PDF(s) (should|should NOT) allow "<action>"` (actions: `printing`, `copying`, `modifying`, `annotating`)
|
- `the (response|webhook request) PDF(s) (should|should NOT) allow "<action>"` (actions: `printing`, `copying`, `modifying`, `annotating`)
|
||||||
- `the (response|webhook request) PDF(s) (should|should NOT) have the "<filename>" file embedded`
|
- `the (response|webhook request) PDF(s) (should|should NOT) have the "<filename>" file embedded`
|
||||||
- `the "<name>" PDF should have <N> image(s)`
|
- `the "<name>" PDF should have <N> image(s)`
|
||||||
|
- `the "<name>" PDF should reuse <N> image XObject(s) across <P> page(s)` (checks image appearances on every page and distinct PDF object IDs)
|
||||||
- `the Gotenberg container (should|should NOT) log the following entries:` (table of log substrings)
|
- `the Gotenberg container (should|should NOT) log the following entries:` (table of log substrings)
|
||||||
- `all concurrent response status codes should be <code>`
|
- `all concurrent response status codes should be <code>`
|
||||||
- `all concurrent responses should have <N> PDF(s)`
|
- `all concurrent responses should have <N> PDF(s)`
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
@chromium
|
||||||
|
@chromium-convert-html
|
||||||
|
@chromium-image-deduplication
|
||||||
|
Feature: Chromium header and footer image deduplication
|
||||||
|
|
||||||
|
# See: https://github.com/gotenberg/gotenberg/issues/1077.
|
||||||
|
# See: https://github.com/gotenberg/gotenberg/issues/1669.
|
||||||
|
# The PNG must appear on every page, but its image XObject must be shared.
|
||||||
|
Scenario: POST /forms/chromium/convert/html (Header image reused across pages)
|
||||||
|
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/page-1-html/index.html | file |
|
||||||
|
| files | testdata/header-footer-image/header.html | file |
|
||||||
|
| marginTop | 1 | field |
|
||||||
|
| marginBottom | 1 | field |
|
||||||
|
| Gotenberg-Output-Filename | header | 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 "header.pdf" PDF should have 1 page(s)
|
||||||
|
Then the "header.pdf" PDF should reuse 1 image XObject(s) across 1 page(s)
|
||||||
|
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/pages-3-html/index.html | file |
|
||||||
|
| files | testdata/header-footer-image/header.html | file |
|
||||||
|
| marginTop | 1 | field |
|
||||||
|
| marginBottom | 1 | field |
|
||||||
|
| Gotenberg-Output-Filename | header | 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 "header.pdf" PDF should have 3 page(s)
|
||||||
|
Then the "header.pdf" PDF should reuse 1 image XObject(s) across 3 page(s)
|
||||||
|
|
||||||
|
Scenario: POST /forms/chromium/convert/html (Footer image reused across pages)
|
||||||
|
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/page-1-html/index.html | file |
|
||||||
|
| files | testdata/header-footer-image/footer.html | file |
|
||||||
|
| marginTop | 1 | field |
|
||||||
|
| marginBottom | 1 | field |
|
||||||
|
| Gotenberg-Output-Filename | footer | 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 "footer.pdf" PDF should have 1 page(s)
|
||||||
|
Then the "footer.pdf" PDF should reuse 1 image XObject(s) across 1 page(s)
|
||||||
|
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/pages-3-html/index.html | file |
|
||||||
|
| files | testdata/header-footer-image/footer.html | file |
|
||||||
|
| marginTop | 1 | field |
|
||||||
|
| marginBottom | 1 | field |
|
||||||
|
| Gotenberg-Output-Filename | footer | 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 "footer.pdf" PDF should have 3 page(s)
|
||||||
|
Then the "footer.pdf" PDF should reuse 1 image XObject(s) across 3 page(s)
|
||||||
72
test/integration/scenario/images.go
Normal file
72
test/integration/scenario/images.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package scenario
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *scenario) thePdfShouldReuseImageXObjects(ctx context.Context, name string, images, pages int) error {
|
||||||
|
path := filepath.Join(s.workdir, s.resp.Header().Get("Gotenberg-Trace"), name)
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
return fmt.Errorf("stat PDF %q: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := []string{"pdfimages", "-list", filepath.Base(path)}
|
||||||
|
output, err := execCommandInIntegrationToolsContainer(ctx, cmd, path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("exec %q: %w", cmd, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return checkImageXObjectReuse(output, images, pages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkImageXObjectReuse(output string, images, pages int) error {
|
||||||
|
lines := strings.Split(strings.TrimSpace(output), "\n")
|
||||||
|
if len(lines) < 2 || !strings.HasPrefix(strings.TrimSpace(lines[0]), "page") {
|
||||||
|
return fmt.Errorf("invalid pdfimages -list output: %q", output)
|
||||||
|
}
|
||||||
|
|
||||||
|
objects := make(map[[2]int]struct{})
|
||||||
|
imagesPerPage := make(map[int]int)
|
||||||
|
for _, line := range lines[2:] {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 12 {
|
||||||
|
return fmt.Errorf("invalid pdfimages -list row: %q", line)
|
||||||
|
}
|
||||||
|
|
||||||
|
page, err := strconv.Atoi(fields[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse image page %q: %w", fields[0], err)
|
||||||
|
}
|
||||||
|
if page < 1 || page > pages {
|
||||||
|
return fmt.Errorf("image on page %d outside expected range 1-%d", page, pages)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfimages lists appearances, including repeated references to the
|
||||||
|
// same image. Columns 11 and 12 identify the underlying PDF object.
|
||||||
|
var ref [2]int
|
||||||
|
for i := range ref {
|
||||||
|
ref[i], err = strconv.Atoi(fields[10+i])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse image object ID in %q: %w", line, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
objects[ref] = struct{}{}
|
||||||
|
imagesPerPage[page]++
|
||||||
|
}
|
||||||
|
|
||||||
|
for page := 1; page <= pages; page++ {
|
||||||
|
if imagesPerPage[page] != images {
|
||||||
|
return fmt.Errorf("expected %d image appearance(s) on page %d, but actual is %d", images, page, imagesPerPage[page])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(objects) != images {
|
||||||
|
return fmt.Errorf("expected %d unique image XObject(s) shared across %d page(s), but actual is %d", images, pages, len(objects))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
106
test/integration/scenario/images_test.go
Normal file
106
test/integration/scenario/images_test.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package scenario
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheckImageXObjectReuse(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const header = `page num type width height color comp bpc enc interp object ID x-ppi y-ppi size ratio
|
||||||
|
--------------------------------------------------------------------------------------------
|
||||||
|
`
|
||||||
|
imageRow := func(page, object, generation int) string {
|
||||||
|
return fmt.Sprintf("%d %d image 32 32 rgb 3 8 image no %d %d 96 96 3104B 101%%\n", page, page-1, object, generation)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
output string
|
||||||
|
pages int
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "one page baseline",
|
||||||
|
output: header + imageRow(1, 7, 0),
|
||||||
|
pages: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "three pages share one image object",
|
||||||
|
output: header + imageRow(1, 7, 0) + imageRow(2, 7, 0) + imageRow(3, 7, 0),
|
||||||
|
pages: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regression embeds a different object per page",
|
||||||
|
output: header + imageRow(1, 7, 0) + imageRow(2, 8, 0) + imageRow(3, 9, 0),
|
||||||
|
pages: 3,
|
||||||
|
wantErr: "expected 1 unique image XObject(s) shared across 3 page(s), but actual is 3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "object generation is part of its identity",
|
||||||
|
output: header + imageRow(1, 7, 0) + imageRow(2, 7, 1) + imageRow(3, 7, 0),
|
||||||
|
pages: 3,
|
||||||
|
wantErr: "expected 1 unique image XObject(s) shared across 3 page(s), but actual is 2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "image missing from a page",
|
||||||
|
output: header + imageRow(1, 7, 0) + imageRow(3, 7, 0),
|
||||||
|
pages: 3,
|
||||||
|
wantErr: "expected 1 image appearance(s) on page 2, but actual is 0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no image cannot pass",
|
||||||
|
output: header,
|
||||||
|
pages: 1,
|
||||||
|
wantErr: "expected 1 image appearance(s) on page 1, but actual is 0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "repeated image only on first page cannot pass",
|
||||||
|
output: header + imageRow(1, 7, 0) + imageRow(1, 7, 0) + imageRow(1, 7, 0),
|
||||||
|
pages: 3,
|
||||||
|
wantErr: "expected 1 image appearance(s) on page 1, but actual is 3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "image on unexpected page",
|
||||||
|
output: header + imageRow(1, 7, 0) + imageRow(2, 7, 0),
|
||||||
|
pages: 1,
|
||||||
|
wantErr: "image on page 2 outside expected range 1-1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tool error cannot pass",
|
||||||
|
output: "Syntax Error: Couldn't find trailer dictionary",
|
||||||
|
pages: 1,
|
||||||
|
wantErr: "invalid pdfimages -list output:",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "malformed image row",
|
||||||
|
output: header + "1 0 image\n",
|
||||||
|
pages: 1,
|
||||||
|
wantErr: "invalid pdfimages -list row:",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "inline image has no reusable object ID",
|
||||||
|
output: header + "1 0 image 32 32 rgb 3 8 image no [inline] - 96 96 3104B 101%\n",
|
||||||
|
pages: 1,
|
||||||
|
wantErr: "parse image object ID",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
err := checkImageXObjectReuse(tt.output, 1, tt.pages)
|
||||||
|
if tt.wantErr == "" {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||||
|
t.Fatalf("expected error containing %q, got %v", tt.wantErr, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1799,6 +1799,7 @@ func InitializeScenario(ctx *godog.ScenarioContext) {
|
|||||||
ctx.Then(`^the "([^"]*)" PDF (should|should NOT) have the following content at page (\d+):$`, s.thePdfShouldHaveTheFollowingContentAtPage)
|
ctx.Then(`^the "([^"]*)" PDF (should|should NOT) have the following content at page (\d+):$`, s.thePdfShouldHaveTheFollowingContentAtPage)
|
||||||
ctx.Then(`^the "([^"]*)" PDF (should|should NOT) have content matching "([^"]*)" at page (\d+)$`, s.thePdfShouldHaveContentMatchingAtPage)
|
ctx.Then(`^the "([^"]*)" PDF (should|should NOT) have content matching "([^"]*)" at page (\d+)$`, s.thePdfShouldHaveContentMatchingAtPage)
|
||||||
ctx.Then(`^the "([^"]*)" PDF should have (\d+) image\(s\)$`, s.thePdfShouldHaveImages)
|
ctx.Then(`^the "([^"]*)" PDF should have (\d+) image\(s\)$`, s.thePdfShouldHaveImages)
|
||||||
|
ctx.Then(`^the "([^"]*)" PDF should reuse (\d+) image XObject\(s\) across (\d+) page\(s\)$`, s.thePdfShouldReuseImageXObjects)
|
||||||
ctx.Then(`^the "([^"]*)" image should be (\d+)x(\d+) pixels$`, s.theImageShouldBePixels)
|
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 "([^"]*)" image pixel at (\d+),(\d+) should be "([^"]*)"$`, s.theImagePixelShouldBe)
|
||||||
ctx.Then(`^the "([^"]*)" file size should be (less|greater) than (\d+) KB$`, s.theFileSizeShouldBe)
|
ctx.Then(`^the "([^"]*)" file size should be (less|greater) than (\d+) KB$`, s.theFileSizeShouldBe)
|
||||||
|
|||||||
15
test/integration/testdata/header-footer-image/footer.html
vendored
Normal file
15
test/integration/testdata/header-footer-image/footer.html
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; width: 100%">
|
||||||
|
<!-- An opaque RGB PNG avoids a separate alpha-mask image XObject. -->
|
||||||
|
<img
|
||||||
|
alt="Regression test logo"
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
src="data:image/png;base64,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"
|
||||||
|
/>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
15
test/integration/testdata/header-footer-image/header.html
vendored
Normal file
15
test/integration/testdata/header-footer-image/header.html
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; width: 100%">
|
||||||
|
<!-- An opaque RGB PNG avoids a separate alpha-mask image XObject. -->
|
||||||
|
<img
|
||||||
|
alt="Regression test logo"
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
src="data:image/png;base64,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"
|
||||||
|
/>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user