Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions internal/testutil/pdf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package testutil
import (
"bytes"
"errors"
"fmt"
"image"
"image/color"
"image/png"
Expand Down Expand Up @@ -112,6 +113,102 @@ func TestInspectD2PDFKeepsSameTitleBoardsDistinct(t *testing.T) {
}
}

func TestPDFPageFillDoesNotDependOnNavigation(t *testing.T) {
for _, fill := range []struct {
name, value, operator, textOperator string
}{
{"dark", "#174ea6", "0.090 0.306 0.651 rg", "1.000 g"},
{"light", "#f2cf9b", "0.949 0.812 0.608 rg", "0.039 0.059 0.145 rg"},
{"empty", "", "1.000 g", "0.039 0.059 0.145 rg"},
{"transparent", "TrAnSpArEnT", "1.000 g", "0.039 0.059 0.145 rg"},
} {
for _, includeNav := range []bool{false, true} {
t.Run(fmt.Sprintf("%s/nav=%t", fill.name, includeNav), func(t *testing.T) {
t.Parallel()
// The tiny, partly transparent image leaves most of the page
// uncovered. Its own pixels cannot supply the page background.
pngBytes := pdfTestPNG(t)
document := pdf.Init()
parent := pdf.BoardTitle{Name: "Parent", BoardID: "root"}
child := pdf.BoardTitle{Name: "Child", BoardID: "root.layers.child"}
pageMap := map[string]int{parent.BoardID: 0, child.BoardID: 1}
for _, path := range [][]pdf.BoardTitle{{parent}, {parent, child}} {
if err := document.AddPDFPage(pngBytes, path, 0, fill.value, nil, 0, 0, 0, pageMap, includeNav); err != nil {
t.Fatal(err)
}
}
var output bytes.Buffer
if err := document.ExportTo(&output); err != nil {
t.Fatal(err)
}
inspection, err := InspectD2PDF(output.Bytes())
if err != nil {
t.Fatal(err)
}
if len(inspection.Pages) != 2 {
t.Fatalf("page count = %d, want 2", len(inspection.Pages))
}
if _, err := ComparePDFImages(output.Bytes(), [][]byte{pngBytes}, imagediff.Options{}); err != nil {
t.Fatal(err)
}
objects, err := splitPDFObjects(output.Bytes())
if err != nil {
t.Fatal(err)
}
byNumber := make(map[int]pdfObject, len(objects))
for _, object := range objects {
byNumber[object.number] = object
}
height := 576.0
if includeNav {
height += 72
}
for index, page := range inspection.Pages {
if page.Width != 576 || page.Height != height {
t.Fatalf("page %d dimensions = %.2fx%.2f, want 576x%.2f", index+1, page.Width, page.Height, height)
}
pageObject := byNumber[page.ObjectNumber]
contentNumber, err := pdfDictionaryInteger(pageObject.body, "Contents")
if err != nil {
t.Fatal(err)
}
content, err := decodePDFContentStream(byNumber[contentNumber])
if err != nil {
t.Fatal(err)
}
// Check the actual exported page stream, not the writer's
// remembered fill color. Paint the whole page once, below
// the image so transparent pixels reveal the selected fill.
rectangle := []byte(fmt.Sprintf("0.00 %.2f 576.00 -%.2f re f", height, height))
background := append([]byte(fill.operator+"\n"), rectangle...)
fillIndex := bytes.Index(content, background)
imageUse := pdfImageUseRE.FindIndex(content)
if fillIndex < 0 || bytes.Count(content, rectangle) != 1 || imageUse == nil || fillIndex >= imageUse[0] {
t.Fatalf("page %d must paint %q once before its image; stream:\n%s", index+1, background, content)
}
if includeNav {
if !bytes.Contains(content, []byte("\x00P\x00a\x00r\x00e\x00n\x00t")) ||
(index == 1 && !bytes.Contains(content, []byte("\x00C\x00h\x00i\x00l\x00d"))) {
t.Fatalf("page %d lost its navigation text", index+1)
}
if !bytes.Contains(content, []byte("q "+fill.textOperator+" BT")) {
t.Fatalf("page %d lost contrasting header text color %q", index+1, fill.textOperator)
}
if page.InternalLinks != index {
t.Fatalf("page %d internal links = %d, want %d", index+1, page.InternalLinks, index)
}
if index == 1 && !bytes.Contains(pageObject.body, []byte(fmt.Sprintf("/Dest [%d 0 R", inspection.Pages[0].ObjectNumber))) {
t.Fatal("child breadcrumb no longer targets the physical parent page")
}
} else if page.InternalLinks != 0 || bytes.Contains(content, []byte("Tj")) || bytes.Contains(content, []byte("TJ")) {
t.Fatalf("page %d gained a navigation header or link", index+1)
}
}
})
}
}
}

func pdfTestPNG(t *testing.T) []byte {
t.Helper()
img := image.NewNRGBA(image.Rect(0, 0, 3, 2))
Expand Down
4 changes: 2 additions & 2 deletions lib/pdf/pdf.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,11 @@ func (g *GoFPDF) AddPDFPage(png []byte, titlePath []BoardTitle, themeID int64, f

// Add page
g.pdf.AddPageFormat("", fpdf.SizeType{Wd: pageWidth, Ht: pageHeight + headerHeight})
g.pdf.SetFillColor(int(fillRGB.Red), int(fillRGB.Green), int(fillRGB.Blue))
g.pdf.Rect(0, 0, pageWidth, pageHeight+headerHeight, "F")

if includeNav {
// Draw header
g.pdf.SetFillColor(int(fillRGB.Red), int(fillRGB.Green), int(fillRGB.Blue))
g.pdf.Rect(0, 0, pageWidth, pageHeight+headerHeight, "F")
if fillRGB.IsLight() {
g.pdf.SetTextColor(10, 15, 37) // steel-900
} else {
Expand Down
Loading