test(chromium): cover header and footer image deduplication

This commit is contained in:
Antonio Pauletich
2026-10-02 21:13:46 +02:00
committed by Julien Neuhart
parent cc9cd5d82d
commit c83dbd7baf
8 changed files with 269 additions and 1 deletions

View 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
}

View 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)
}
})
}
}

View File

@@ -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 content matching "([^"]*)" at page (\d+)$`, s.thePdfShouldHaveContentMatchingAtPage)
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 pixel at (\d+),(\d+) should be "([^"]*)"$`, s.theImagePixelShouldBe)
ctx.Then(`^the "([^"]*)" file size should be (less|greater) than (\d+) KB$`, s.theFileSizeShouldBe)