mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-16 12:22:17 +01:00
feat: add embeds metadata
This commit is contained in:
committed by
Julien Neuhart
parent
eff9444294
commit
3187980ead
271
pkg/modules/qpdf/qpdf_test.go
Normal file
271
pkg/modules/qpdf/qpdf_test.go
Normal file
@@ -0,0 +1,271 @@
|
||||
package qpdf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStripQpdfStringPrefix(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"unicode prefix", "u:factur-x.xml", "factur-x.xml"},
|
||||
{"binary prefix", "b:binary.bin", "binary.bin"},
|
||||
{"encoded prefix", "e:encoded.txt", "encoded.txt"},
|
||||
{"no prefix", "plain.xml", "plain.xml"},
|
||||
{"empty string", "", ""},
|
||||
{"prefix only", "u:", ""},
|
||||
{"colon in value", "u:file:name.xml", "file:name.xml"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := stripQpdfStringPrefix(tt.input)
|
||||
if got != tt.expected {
|
||||
t.Errorf("stripQpdfStringPrefix(%q) = %q, want %q", tt.input, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePdfObjects(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantKeys []string
|
||||
wantError bool
|
||||
}{
|
||||
{
|
||||
name: "valid QPDF JSON v2",
|
||||
input: `{"qpdf":[{"jsonversion":2},{"obj:1 0 R":{"value":{"/Type":"/Catalog"}}}]}`,
|
||||
wantKeys: []string{"obj:1 0 R"},
|
||||
},
|
||||
{
|
||||
name: "invalid JSON",
|
||||
input: `not json`,
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "empty qpdf array",
|
||||
input: `{"qpdf":[]}`,
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "only header element",
|
||||
input: `{"qpdf":[{"jsonversion":2}]}`,
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "multiple objects",
|
||||
input: `{"qpdf":[{},{"obj:1 0 R":{"value":{}},"obj:2 0 R":{"value":{}}}]}`,
|
||||
wantKeys: []string{"obj:1 0 R", "obj:2 0 R"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
objects, err := parsePdfObjects([]byte(tt.input))
|
||||
if tt.wantError {
|
||||
if err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
for _, key := range tt.wantKeys {
|
||||
if _, ok := objects[key]; !ok {
|
||||
t.Errorf("expected key %q in objects", key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatchFilespecMetadata(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
|
||||
|
||||
t.Run("sets AFRelationship on matching Filespec", func(t *testing.T) {
|
||||
objects := map[string]json.RawMessage{
|
||||
"obj:1 0 R": json.RawMessage(`{"value":{"/Type":"/Catalog"}}`),
|
||||
"obj:2 0 R": json.RawMessage(`{"value":{"/Type":"/Filespec","/UF":"u:factur-x.xml"}}`),
|
||||
}
|
||||
metadata := map[string]map[string]string{
|
||||
"factur-x.xml": {"relationship": "Data"},
|
||||
}
|
||||
|
||||
catalogRef, _, filespecRefs, updateObjects := patchFilespecMetadata(logger, objects, metadata)
|
||||
|
||||
if catalogRef != "obj:1 0 R" {
|
||||
t.Errorf("catalogRef = %q, want %q", catalogRef, "obj:1 0 R")
|
||||
}
|
||||
if len(filespecRefs) != 1 || filespecRefs[0] != "obj:2 0 R" {
|
||||
t.Errorf("filespecRefs = %v, want [obj:2 0 R]", filespecRefs)
|
||||
}
|
||||
updated, ok := updateObjects["obj:2 0 R"]
|
||||
if !ok {
|
||||
t.Fatal("expected obj:2 0 R in updateObjects")
|
||||
}
|
||||
value := updated.(map[string]any)["value"].(map[string]any)
|
||||
if value["/AFRelationship"] != "/Data" {
|
||||
t.Errorf("/AFRelationship = %v, want /Data", value["/AFRelationship"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("skips Filespec with no matching metadata", func(t *testing.T) {
|
||||
objects := map[string]json.RawMessage{
|
||||
"obj:1 0 R": json.RawMessage(`{"value":{"/Type":"/Filespec","/UF":"u:other.xml"}}`),
|
||||
}
|
||||
metadata := map[string]map[string]string{
|
||||
"factur-x.xml": {"relationship": "Data"},
|
||||
}
|
||||
|
||||
_, _, filespecRefs, _ := patchFilespecMetadata(logger, objects, metadata)
|
||||
if len(filespecRefs) != 0 {
|
||||
t.Errorf("filespecRefs = %v, want empty", filespecRefs)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("falls back to /F when /UF is absent", func(t *testing.T) {
|
||||
objects := map[string]json.RawMessage{
|
||||
"obj:1 0 R": json.RawMessage(`{"value":{"/Type":"/Filespec","/F":"u:factur-x.xml"}}`),
|
||||
}
|
||||
metadata := map[string]map[string]string{
|
||||
"factur-x.xml": {"relationship": "Alternative"},
|
||||
}
|
||||
|
||||
_, _, filespecRefs, updateObjects := patchFilespecMetadata(logger, objects, metadata)
|
||||
if len(filespecRefs) != 1 {
|
||||
t.Fatalf("filespecRefs = %v, want 1 entry", filespecRefs)
|
||||
}
|
||||
value := updateObjects["obj:1 0 R"].(map[string]any)["value"].(map[string]any)
|
||||
if value["/AFRelationship"] != "/Alternative" {
|
||||
t.Errorf("/AFRelationship = %v, want /Alternative", value["/AFRelationship"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sets stream Subtype via EF reference", func(t *testing.T) {
|
||||
objects := map[string]json.RawMessage{
|
||||
"obj:2 0 R": json.RawMessage(`{"value":{"/Type":"/Filespec","/UF":"u:factur-x.xml","/EF":{"/F":"3 0 R"}}}`),
|
||||
"obj:3 0 R": json.RawMessage(`{"stream":{"dict":{"/Type":"/EmbeddedFile"}}}`),
|
||||
}
|
||||
metadata := map[string]map[string]string{
|
||||
"factur-x.xml": {"mimeType": "text/xml"},
|
||||
}
|
||||
|
||||
_, _, _, updateObjects := patchFilespecMetadata(logger, objects, metadata)
|
||||
streamObj, ok := updateObjects["obj:3 0 R"]
|
||||
if !ok {
|
||||
t.Fatal("expected obj:3 0 R in updateObjects")
|
||||
}
|
||||
stream := streamObj.(map[string]any)["stream"].(map[string]any)
|
||||
dict := stream["dict"].(map[string]any)
|
||||
if dict["/Subtype"] != "/text/xml" {
|
||||
t.Errorf("/Subtype = %v, want /text/xml", dict["/Subtype"])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPatchCatalogAF(t *testing.T) {
|
||||
t.Run("adds filespec refs to AF array", func(t *testing.T) {
|
||||
catalogValue := map[string]any{"/Type": "/Catalog"}
|
||||
updateObjects := make(map[string]any)
|
||||
|
||||
patchCatalogAF("obj:1 0 R", catalogValue, []string{"obj:2 0 R", "obj:3 0 R"}, updateObjects)
|
||||
|
||||
af, ok := catalogValue["/AF"].([]any)
|
||||
if !ok {
|
||||
t.Fatal("expected /AF to be []any")
|
||||
}
|
||||
if len(af) != 2 {
|
||||
t.Fatalf("/AF has %d entries, want 2", len(af))
|
||||
}
|
||||
if af[0] != "2 0 R" || af[1] != "3 0 R" {
|
||||
t.Errorf("/AF = %v, want [2 0 R, 3 0 R]", af)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("does not duplicate existing refs", func(t *testing.T) {
|
||||
catalogValue := map[string]any{
|
||||
"/Type": "/Catalog",
|
||||
"/AF": []any{"2 0 R"},
|
||||
}
|
||||
updateObjects := make(map[string]any)
|
||||
|
||||
patchCatalogAF("obj:1 0 R", catalogValue, []string{"obj:2 0 R", "obj:3 0 R"}, updateObjects)
|
||||
|
||||
af := catalogValue["/AF"].([]any)
|
||||
if len(af) != 2 {
|
||||
t.Fatalf("/AF has %d entries, want 2", len(af))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no-op when catalogRef is empty", func(t *testing.T) {
|
||||
updateObjects := make(map[string]any)
|
||||
patchCatalogAF("", nil, []string{"obj:2 0 R"}, updateObjects)
|
||||
if len(updateObjects) != 0 {
|
||||
t.Error("expected no updates for empty catalogRef")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetStreamSubtype(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
|
||||
|
||||
t.Run("sets Subtype in stream dict", func(t *testing.T) {
|
||||
objects := map[string]json.RawMessage{
|
||||
"obj:3 0 R": json.RawMessage(`{"stream":{"dict":{"/Type":"/EmbeddedFile"}}}`),
|
||||
}
|
||||
updateObjects := make(map[string]any)
|
||||
|
||||
setStreamSubtype(logger, objects, updateObjects, "obj:3 0 R", "text/xml")
|
||||
|
||||
streamObj := updateObjects["obj:3 0 R"].(map[string]any)["stream"].(map[string]any)
|
||||
dict := streamObj["dict"].(map[string]any)
|
||||
if dict["/Subtype"] != "/text/xml" {
|
||||
t.Errorf("/Subtype = %v, want /text/xml", dict["/Subtype"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("auto-adds obj: prefix to ref", func(t *testing.T) {
|
||||
objects := map[string]json.RawMessage{
|
||||
"obj:5 0 R": json.RawMessage(`{"stream":{"dict":{}}}`),
|
||||
}
|
||||
updateObjects := make(map[string]any)
|
||||
|
||||
setStreamSubtype(logger, objects, updateObjects, "5 0 R", "application/pdf")
|
||||
|
||||
if _, ok := updateObjects["obj:5 0 R"]; !ok {
|
||||
t.Error("expected obj:5 0 R in updateObjects")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("warns on missing object", func(t *testing.T) {
|
||||
objects := map[string]json.RawMessage{}
|
||||
updateObjects := make(map[string]any)
|
||||
|
||||
setStreamSubtype(logger, objects, updateObjects, "obj:99 0 R", "text/xml")
|
||||
|
||||
if len(updateObjects) != 0 {
|
||||
t.Error("expected no updates for missing object")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("warns on object without stream key", func(t *testing.T) {
|
||||
objects := map[string]json.RawMessage{
|
||||
"obj:3 0 R": json.RawMessage(`{"value":{"/Type":"/Page"}}`),
|
||||
}
|
||||
updateObjects := make(map[string]any)
|
||||
|
||||
setStreamSubtype(logger, objects, updateObjects, "obj:3 0 R", "text/xml")
|
||||
|
||||
if len(updateObjects) != 0 {
|
||||
t.Error("expected no updates for non-stream object")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user