2020-07-01 19:51:28 +00:00

183 lines
4.3 KiB
Go

/*
* This file is subject to the terms and conditions defined in
* file 'LICENSE.md', which is part of this source code package.
*/
package testutils
import (
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
"image"
"image/png"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
// Errors.
var (
ErrRenderNotSupported = errors.New("rendering PDF files is not supported on this system")
)
// CopyFile copies the `src` file to `dst`.
func CopyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
return err
}
// ReadPNG reads and returns the specified PNG file.
func ReadPNG(file string) (image.Image, error) {
f, err := os.Open(file)
if err != nil {
return nil, err
}
defer f.Close()
return png.Decode(f)
}
// HashFile generates an MD5 hash from the contents of the specified file.
func HashFile(file string) (string, error) {
f, err := os.Open(file)
if err != nil {
return "", err
}
defer f.Close()
h := md5.New()
if _, err = io.Copy(h, f); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
// CompareImages compares the specified images at pixel level.
func CompareImages(img1, img2 image.Image) (bool, error) {
rect := img1.Bounds()
diff := 0
for x := 0; x < rect.Size().X; x++ {
for y := 0; y < rect.Size().Y; y++ {
r1, g1, b1, _ := img1.At(x, y).RGBA()
r2, g2, b2, _ := img2.At(x, y).RGBA()
if r1 != r2 || g1 != g2 || b1 != b2 {
diff++
}
}
}
diffFraction := float64(diff) / float64(rect.Dx()*rect.Dy())
if diffFraction > 0.0001 {
fmt.Printf("diff fraction: %v (%d)\n", diffFraction, diff)
return false, nil
}
return true, nil
}
// ComparePNGFiles compares the specified PNG files.
func ComparePNGFiles(file1, file2 string) (bool, error) {
// Fast path - compare hashes.
h1, err := HashFile(file1)
if err != nil {
return false, err
}
h2, err := HashFile(file2)
if err != nil {
return false, err
}
if h1 == h2 {
return true, nil
}
// Slow path - compare pixel by pixel.
img1, err := ReadPNG(file1)
if err != nil {
return false, err
}
img2, err := ReadPNG(file2)
if err != nil {
return false, err
}
if img1.Bounds() != img2.Bounds() {
return false, nil
}
return CompareImages(img1, img2)
}
// RenderPDFToPNGs uses ghostscript (gs) to render the specified PDF file into
// a set of PNG images (one per page). PNG images will be named xxx-N.png where
// N is the number of page, starting from 1.
func RenderPDFToPNGs(pdfPath string, dpi int, outpathTpl string) error {
if dpi <= 0 {
dpi = 100
}
if _, err := exec.LookPath("gs"); err != nil {
return ErrRenderNotSupported
}
return exec.Command("gs", "-sDEVICE=pngalpha", "-o", outpathTpl, fmt.Sprintf("-r%d", dpi), pdfPath).Run()
}
// RunRenderTest renders the PDF file specified by `pdfPath` to the `outputDir`
// and compares the output PNG files to the golden files found at the
// `baselineRenderPath` location. If the specified PDF file is new (there are
// no golden files) and the `saveBaseline` parameter is set to true, the
// output render files are saved to the `baselineRenderPath`.
func RunRenderTest(t *testing.T, pdfPath, outputDir, baselineRenderPath string, saveBaseline bool) {
tplName := strings.TrimSuffix(filepath.Base(pdfPath), filepath.Ext(pdfPath))
t.Run("render", func(t *testing.T) {
imgPathPrefix := filepath.Join(outputDir, tplName)
imgPathTpl := imgPathPrefix + "-%d.png"
if err := RenderPDFToPNGs(pdfPath, 0, imgPathTpl); err != nil {
t.Skip(err)
}
for i := 1; true; i++ {
imgPath := fmt.Sprintf("%s-%d.png", imgPathPrefix, i)
expImgPath := filepath.Join(baselineRenderPath, fmt.Sprintf("%s-%d_exp.png", tplName, i))
if _, err := os.Stat(imgPath); err != nil {
break
}
t.Logf("%s", expImgPath)
if _, err := os.Stat(expImgPath); os.IsNotExist(err) {
if saveBaseline {
t.Logf("Copying %s -> %s", imgPath, expImgPath)
CopyFile(imgPath, expImgPath)
continue
}
break
}
t.Run(fmt.Sprintf("page%d", i), func(t *testing.T) {
t.Logf("Comparing %s vs %s", imgPath, expImgPath)
ok, err := ComparePNGFiles(imgPath, expImgPath)
if os.IsNotExist(err) {
t.Fatal("image file missing")
} else if !ok {
t.Fatal("wrong page rendered")
}
})
}
})
}