diff --git a/internal/testutil/pdf_test.go b/internal/testutil/pdf_test.go index 228c06d940..121b2350c9 100644 --- a/internal/testutil/pdf_test.go +++ b/internal/testutil/pdf_test.go @@ -3,6 +3,7 @@ package testutil import ( "bytes" "errors" + "fmt" "image" "image/color" "image/png" @@ -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)) diff --git a/lib/pdf/pdf.go b/lib/pdf/pdf.go index 398b37d6cc..0894ef3400 100644 --- a/lib/pdf/pdf.go +++ b/lib/pdf/pdf.go @@ -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 {