SkQP: make_gmkb, gm_knowledge (GM Knowledgebase)

Add a real implementation for gm_knowledge.h  This depends on
the presence of files in the form $GMK_DIR/foo/{max,min}.png

The implementation also writes out failures in a report directory.

Add a utility: experimental/make_gmkb which is a stand-alone
go executable that generates the foo/{max,min}.png data.

tools/skqp/README.md has instructions on running SkQP.

Also: add SkFontMgrPriv.h

Change-Id: Ibe1e9a7e7de143d14eee3877f5f2d2d8713f7f49
Reviewed-on: https://skia-review.googlesource.com/65380
Reviewed-by: Yuqian Li <liyuqian@google.com>
Commit-Queue: Hal Canary <halcanary@google.com>
diff --git a/tools/skqp/make_gmkb.go b/tools/skqp/make_gmkb.go
new file mode 100644
index 0000000..b8b67a3
--- /dev/null
+++ b/tools/skqp/make_gmkb.go
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+package main
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"image"
+	"image/draw"
+	"image/png"
+	"log"
+	"net/http"
+	"os"
+	"path"
+	"sort"
+	"strings"
+	"sync"
+
+	"go.skia.org/infra/golden/go/search"
+)
+
+const (
+	min_png = "min.png"
+	max_png = "max.png"
+)
+
+type ExportTestRecordArray []search.ExportTestRecord
+
+func (a ExportTestRecordArray) Len() int           { return len(a) }
+func (a ExportTestRecordArray) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
+func (a ExportTestRecordArray) Less(i, j int) bool { return a[i].TestName < a[j].TestName }
+
+func in(v string, a []string) bool {
+	for _, u := range a {
+		if u == v {
+			return true
+		}
+	}
+	return false
+}
+
+// TODO(halcanary): clean up this blacklist.
+var blacklist = []string{
+	"circular-clips",
+	"colorcomposefilter_wacky",
+	"coloremoji_blendmodes",
+	"colormatrix",
+	"complexclip_bw",
+	"complexclip_bw_invert",
+	"complexclip_bw_layer",
+	"complexclip_bw_layer_invert",
+	"convex-lineonly-paths-stroke-and-fill",
+	"dftext",
+	"downsamplebitmap_image_high_mandrill_512.png",
+	"downsamplebitmap_image_medium_mandrill_512.png",
+	"filterbitmap_image_mandrill_16.png",
+	"filterbitmap_image_mandrill_64.png",
+	"filterbitmap_image_mandrill_64.png_g8",
+	"gradients_degenerate_2pt",
+	"gradients_degenerate_2pt_nodither",
+	"gradients_local_perspective",
+	"gradients_local_perspective_nodither",
+	"imagefilterstransformed",
+	"image_scale_aligned",
+	"lattice",
+	"linear_gradient",
+	"mipmap_srgb",
+	"mixedtextblobs",
+	"OverStroke",
+	"simple-offsetimagefilter",
+	"strokerect",
+	"textblobmixedsizes",
+	"textblobmixedsizes_df"}
+
+func processTest(testName string, imgUrls []string, output string) error {
+	if strings.ContainsRune(testName, '/') {
+		return nil
+	}
+	output_directory := path.Join(output, testName)
+	var img_max image.NRGBA
+	var img_min image.NRGBA
+	for _, url := range imgUrls {
+		resp, err := http.Get(url)
+		if err != nil {
+			return err
+		}
+		img, err := png.Decode(resp.Body)
+		resp.Body.Close()
+		if err != nil {
+			return err
+		}
+		if img_max.Rect.Max.X == 0 {
+			// N.B. img_max.Pix may alias img.Pix (if they're already NRGBA).
+			img_max = toNrgba(img)
+			img_min = copyNrgba(img_max)
+			continue
+		}
+		w := img.Bounds().Max.X - img.Bounds().Min.X
+		h := img.Bounds().Max.Y - img.Bounds().Min.Y
+		if img_max.Rect.Max.X != w || img_max.Rect.Max.Y != h {
+			return errors.New("size mismatch")
+		}
+		img_nrgba := toNrgba(img)
+		for i, value := range img_nrgba.Pix {
+			if value > img_max.Pix[i] {
+				img_max.Pix[i] = value
+			} else if value < img_min.Pix[i] {
+				img_min.Pix[i] = value
+			}
+		}
+	}
+	if img_max.Rect.Max.X == 0 {
+		return nil
+	}
+	if err := os.Mkdir(output_directory, os.ModePerm); err != nil && !os.IsExist(err) {
+		return err
+	}
+	if err := writePngToFile(path.Join(output_directory, min_png), &img_min); err != nil {
+		return err
+	}
+	if err := writePngToFile(path.Join(output_directory, max_png), &img_max); err != nil {
+		return err
+	}
+	return nil
+
+}
+
+func readMetaJsonFile(filename string) ([]search.ExportTestRecord, error) {
+	file, err := os.Open(filename)
+	if err != nil {
+		return nil, err
+	}
+	dec := json.NewDecoder(file)
+	var records []search.ExportTestRecord
+	err = dec.Decode(&records)
+	return records, err
+}
+
+func writePngToFile(path string, img image.Image) error {
+	file, err := os.Create(path)
+	if err != nil {
+		return err
+	}
+	defer file.Close()
+	return png.Encode(file, img)
+}
+
+// to_nrgb() may return a shallow copy of img if it's already NRGBA.
+func toNrgba(img image.Image) image.NRGBA {
+	switch v := img.(type) {
+	case *image.NRGBA:
+		return *v
+	}
+	nimg := *image.NewNRGBA(img.Bounds())
+	draw.Draw(&nimg, img.Bounds(), img, image.Point{0, 0}, draw.Src)
+	return nimg
+}
+
+func copyNrgba(src image.NRGBA) image.NRGBA {
+	dst := image.NRGBA{make([]uint8, len(src.Pix)), src.Stride, src.Rect}
+	copy(dst.Pix, src.Pix)
+	return dst
+}
+
+func main() {
+	if len(os.Args) != 3 {
+		log.Printf("Usage:\n  %s INPUT.json OUTPUT_DIRECTORY\n\n", os.Args[0])
+		os.Exit(1)
+	}
+	input := os.Args[1]
+	output := os.Args[2]
+	err := os.MkdirAll(output, os.ModePerm)
+	if err != nil && !os.IsExist(err) {
+		log.Fatal(err)
+	}
+
+	records, err := readMetaJsonFile(input)
+	if err != nil {
+		log.Fatal(err)
+	}
+	sort.Sort(ExportTestRecordArray(records))
+
+	index, err := os.Create(path.Join(output, "index.txt"))
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer index.Close()
+
+	var wg sync.WaitGroup
+	for _, record := range records {
+		if in(record.TestName, blacklist) {
+			continue
+		}
+		var goodUrls []string
+		for _, digest := range record.Digests {
+			if (in("vk", digest.ParamSet["config"]) ||
+				in("gles", digest.ParamSet["config"])) &&
+				digest.Status == "positive" {
+				goodUrls = append(goodUrls, digest.URL)
+			}
+		}
+		wg.Add(1)
+		go func(testName string, imgUrls []string, output string) {
+			defer wg.Done()
+			if err := processTest(testName, imgUrls, output); err != nil {
+				log.Fatal(err)
+			}
+			fmt.Printf("\r%-60s", testName)
+		}(record.TestName, goodUrls, output)
+
+		_, err = fmt.Fprintf(index, "%s\n", record.TestName)
+		if err != nil {
+			log.Fatal(err)
+		}
+	}
+	wg.Wait()
+	fmt.Printf("\r%60s\n", "")
+}