//go:build cgo && manual package parser import ( "image" "image/color" "image/png" "math" "os" "path/filepath" "strings" "testing" ) // TestRenderCompare renders PDF pages with Go (pdfium) and compares against // Python-rendered images (if available). Outputs to testdata/render_compare/. // // Usage: // 1. Run this test to generate Go renders: // go test -v -tags=manual -run TestRenderCompare -count=1 // 2. Run the Python script to generate Python renders: // python3 testdata/render_compare.py // 3. Re-run this test — it will compare both and report similarity. func TestRenderCompare(t *testing.T) { const dpi = 216.0 pdfDir := filepath.Join("testdata", "pdfs") goDir := filepath.Join("testdata", "output", "render_compare", "go") pyDir := filepath.Join("testdata", "output", "render_compare", "py") os.MkdirAll(goDir, 0755) entries, err := os.ReadDir(pdfDir) if err != nil { t.Fatal(err) } compared := 0 for _, e := range entries { if e.IsDir() || !strings.HasSuffix(strings.ToLower(e.Name()), ".pdf") { continue } name := e.Name() data, err := os.ReadFile(filepath.Join(pdfDir, name)) if err != nil { t.Logf("%s: read error: %v", name, err) continue } eng, err := NewEngine(data) if err != nil { t.Logf("%s: engine error: %v", name, err) continue } // Render page 0 with pdfium (Go). goImg, err := renderPageToImage(eng, 0) eng.Close() if err != nil { t.Logf("%s: render error: %v", name, err) continue } // Save Go render. goPath := filepath.Join(goDir, name+"_p0.png") if err := savePNG(goPath, goImg); err != nil { t.Errorf("%s: save: %v", name, err) continue } goBounds := goImg.Bounds() t.Logf("%s: Go render %dx%d saved", name, goBounds.Dx(), goBounds.Dy()) // Compare with Python render if available. pyPath := filepath.Join(pyDir, name+"_p0.png") pyFile, err := os.Open(pyPath) if err != nil { continue // Python image not available yet } pyImg, err := png.Decode(pyFile) pyFile.Close() if err != nil { t.Logf("%s: decode py image: %v", name, err) continue } sim := pixelSimilarity(goImg, pyImg) compared++ pyBounds := pyImg.Bounds() sizeMatch := goBounds.Dx() == pyBounds.Dx() && goBounds.Dy() == pyBounds.Dy() status := "✅" if sim < 90.0 { status = "⚠️" } if sim < 50.0 { status = "❌" } t.Logf("%s %s: similarity=%.1f%% size Go=%dx%d Py=%dx%d sizeMatch=%v", status, name, sim, goBounds.Dx(), goBounds.Dy(), pyBounds.Dx(), pyBounds.Dy(), sizeMatch) } if compared == 0 { t.Logf("No Python renders found in %s — run: python3 tools/render_compare.py", pyDir) } else { t.Logf("Compared %d PDFs", compared) } } func savePNG(path string, img image.Image) error { f, err := os.Create(path) if err != nil { return err } defer f.Close() return png.Encode(f, img) } // pixelSimilarity computes the percentage of pixels that match within tolerance. // Handles different-sized images by comparing the overlapping region. func pixelSimilarity(a, b image.Image) float64 { ab, bb := a.Bounds(), b.Bounds() w := min(ab.Dx(), bb.Dx()) h := min(ab.Dy(), bb.Dy()) if w == 0 || h == 0 { return 0 } const tolerance = 30 // per-channel tolerance (0-255) matching := 0 for y := 0; y < h; y++ { for x := 0; x < w; x++ { r1, g1, b1, _ := a.At(ab.Min.X+x, ab.Min.Y+y).RGBA() r2, g2, b2, _ := b.At(bb.Min.X+x, bb.Min.Y+y).RGBA() // RGBA() returns 16-bit values; convert to 8-bit. dr := math.Abs(float64(r1>>8) - float64(r2>>8)) dg := math.Abs(float64(g1>>8) - float64(g2>>8)) db := math.Abs(float64(b1>>8) - float64(b2>>8)) if dr <= tolerance && dg <= tolerance && db <= tolerance { matching++ } } } // Penalize size mismatch. maxArea := max(ab.Dx()*ab.Dy(), bb.Dx()*bb.Dy()) if maxArea == 0 { return 0 } return float64(matching) / float64(maxArea) * 100 } func colorDiff(a, b color.Color) float64 { r1, g1, b1, _ := a.RGBA() r2, g2, b2, _ := b.RGBA() dr := float64(r1>>8) - float64(r2>>8) dg := float64(g1>>8) - float64(g2>>8) db := float64(b1>>8) - float64(b2>>8) return math.Sqrt(dr*dr + dg*dg + db*db) }