feat(libreoffice): add generateTaggedPdf form field

Tags the PDF without declaring PDF/UA conformance. Today tagging is only
available through pdfua, which also writes pdfuaid:part=1 into the XMP
metadata. Mirrors the Chromium routes, where generateTaggedPdf and pdfua
are separate fields. Defaults to false, so existing requests are
unchanged.

Closes #1676
This commit is contained in:
Johannes Klein
2026-10-06 10:37:27 +02:00
committed by Julien Neuhart
parent db150b7733
commit e4ee0f29d6
9 changed files with 143 additions and 1 deletions

View File

@@ -1795,6 +1795,7 @@ func InitializeScenario(ctx *godog.ScenarioContext) {
ctx.Then(`^the (response|webhook request) PDF\(s\) should declare Factur-X XMP with conformance level "([^"]*)"$`, s.thePdfsShouldDeclareFacturXConformanceLevel)
ctx.Then(`^the "([^"]*)" PDF should have (\d+) page\(s\)$`, s.thePdfShouldHavePages)
ctx.Then(`^the "([^"]*)" PDF (should|should NOT) be set to landscape orientation$`, s.thePdfShouldBeSetToLandscapeOrientation)
ctx.Then(`^the "([^"]*)" PDF (should|should NOT) be tagged$`, s.thePdfShouldBeTagged)
ctx.Then(`^the "([^"]*)" PDF (should|should NOT) have a document outline$`, s.thePdfShouldHaveADocumentOutline)
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)

View File

@@ -0,0 +1,44 @@
package scenario
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
)
func (s *scenario) thePdfShouldBeTagged(ctx context.Context, name, kind string) 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{"pdfinfo", filepath.Base(path)}
output, err := execCommandInIntegrationToolsContainer(ctx, cmd, path)
if err != nil {
return fmt.Errorf("exec %q: %w", cmd, err)
}
return checkTagged(output, kind != "should NOT")
}
// checkTagged reads the "Tagged" line of pdfinfo, which reflects the Marked
// entry of the document catalog's MarkInfo dictionary.
func checkTagged(output string, want bool) error {
for line := range strings.SplitSeq(output, "\n") {
value, found := strings.CutPrefix(strings.TrimSpace(line), "Tagged:")
if !found {
continue
}
tagged := strings.TrimSpace(value) == "yes"
if tagged != want {
return fmt.Errorf("expected tagged PDF to be %t, but actual is %t", want, tagged)
}
return nil
}
return fmt.Errorf("invalid pdfinfo output, no Tagged line: %q", output)
}

View File

@@ -0,0 +1,67 @@
package scenario
import (
"strings"
"testing"
)
func TestCheckTagged(t *testing.T) {
t.Parallel()
pdfinfo := func(tagged string) string {
return "Producer: LibreOffice 26.8\nTagged: " + tagged + "\nPages: 1\n"
}
tests := []struct {
name string
output string
want bool
wantErr string
}{
{
name: "tagged PDF expected and found",
output: pdfinfo("yes"),
want: true,
},
{
name: "untagged PDF expected and found",
output: pdfinfo("no"),
want: false,
},
{
name: "tagged PDF expected but untagged",
output: pdfinfo("no"),
want: true,
wantErr: "expected tagged PDF to be true, but actual is false",
},
{
name: "untagged PDF expected but tagged",
output: pdfinfo("yes"),
want: false,
wantErr: "expected tagged PDF to be false, but actual is true",
},
{
name: "no Tagged line",
output: "Pages: 1\n",
want: true,
wantErr: "invalid pdfinfo output, no Tagged line:",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
err := checkTagged(tc.output, tc.want)
if tc.wantErr == "" {
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
return
}
if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
t.Fatalf("expected error containing %q, got: %v", tc.wantErr, err)
}
})
}
}