SkQP: fix model colorspace (16-bit gold images)

Problem: `make_gmkb.go `was ignoring the ignoring the embedded ICC
profile in the images it was getting from Gold.

Replace make_gmkb.go with two small programs: `goldgetter.py` and
`make_skqp_model.cpp`.

`make_skqp_model` uses Skia to create the model from a bunch of images.

`goldgetter` wraps `make_skqp_model` and handles:
  - json parsing
  - downloading images from gold
  - multiprocessing

CQ_INCLUDE_TRYBOTS=skia.primary:Build-Debian9-Clang-x86-devrel-Android_SKQP,Test-Debian9-Clang-NUC7i5BNK-CPU-Emulator-x86-devrel-All-Android_SKQP

Change-Id: I7add1a1dfd83bbd0ab07ab126d4183c36325263c
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/209101
Commit-Queue: Hal Canary <halcanary@google.com>
Reviewed-by: Mike Klein <mtklein@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index d5f43f9..51d3510 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -1812,6 +1812,15 @@
     }
   }
 
+  test_app("make_skqp_model") {
+    sources = [
+      "tools/skqp/make_skqp_model.cpp",
+    ]
+    deps = [
+      ":skia",
+    ]
+  }
+
   if (target_cpu != "wasm") {
     import("gn/samples.gni")
     test_lib("samples") {
diff --git a/tools/skqp/cut_release b/tools/skqp/cut_release
index cc92072..a6b199c 100755
--- a/tools/skqp/cut_release
+++ b/tools/skqp/cut_release
@@ -11,21 +11,16 @@
 set -e
 META_JSON="$1"
 cd "$(dirname "$0")/../.."
-
-if [ -z "$SKQP_SKIP_INFRA_UPDATE" ]; then
-    go get -u go.skia.org/infra/golden/go/search
-fi
-go run tools/skqp/make_gmkb.go \
-    "$META_JSON" \
-    platform_tools/android/apps/skqp/src/main/assets/gmkb
 env GIT_SYNC_DEPS_QUIET=1 python tools/git-sync-deps
 O='out/ndebug'
 mkdir -p $O
 bin/gn gen $O --args='cc="clang" cxx="clang++" is_debug=false'
-ninja -C $O jitter_gms list_gpu_unit_tests
+ninja -C $O jitter_gms list_gpu_unit_tests make_skqp_model
+GMKB='platform_tools/android/apps/skqp/src/main/assets/gmkb'
+python tools/skqp/goldgetter.py "$META_JSON" "$GMKB" $O/make_skqp_model
 $O/jitter_gms tools/skqp/bad_gms.txt
 python tools/skqp/make_rendertests_list.py
-rm 'bad.txt' 'good.txt'
+rm 'bad.txt' 'good.txt' "$GMKB"/models.txt
 sh tools/skqp/upload_model
 $O/list_gpu_unit_tests \
     > platform_tools/android/apps/skqp/src/main/assets/skqp/unittests.txt
diff --git a/tools/skqp/goldgetter.py b/tools/skqp/goldgetter.py
new file mode 100755
index 0000000..ba58eb9
--- /dev/null
+++ b/tools/skqp/goldgetter.py
@@ -0,0 +1,48 @@
+#! /usr/bin/env python
+# Copyright 2019 Google LLC.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import json
+import multiprocessing
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+import urllib
+
+def make_skqp_model(arg):
+    name, urls, dst_dir, exe = arg
+    tmp = tempfile.mkdtemp()
+    for url in urls:
+        urllib.urlretrieve(url, tmp + '/' + url[url.rindex('/') + 1:])
+    subprocess.check_call([exe, tmp, dst_dir + '/' + name])
+    shutil.rmtree(tmp)
+    sys.stdout.write(name + ' ')
+    sys.stdout.flush()
+
+def main(meta, dst, exe):
+    assert os.path.exists(exe)
+    jobs = []
+    with open(meta, 'r') as f:
+        for rec in json.load(f):
+            urls = [d['URL'] for d in rec['digests']
+                    if d['status'] == 'positive' and
+                    (set(d['paramset']['config']) & set(['vk', 'gles']))]
+            if urls:
+                jobs.append((rec['testName'], urls, dst, exe))
+    if not os.path.exists(dst):
+        os.mkdir(dst)
+    pool = multiprocessing.Pool(processes=20)
+    pool.map(make_skqp_model, jobs)
+    sys.stdout.write('\n')
+    with open(dst + '/models.txt', 'w') as o:
+        for n, _, _, _ in jobs:
+            o.write(n + '\n')
+
+if __name__ == '__main__':
+    if len(sys.argv) != 4:
+        sys.stderr.write('Usage:\n  %s META.JSON DST_DIR MAKE_SKQP_MODEL_EXE\n\n' % sys.argv[0])
+        sys.exit(1)
+    main(sys.argv[1], sys.argv[2], sys.argv[3])
diff --git a/tools/skqp/make_gmkb.go b/tools/skqp/make_gmkb.go
deleted file mode 100644
index 2b5dda5..0000000
--- a/tools/skqp/make_gmkb.go
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * 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
-}
-
-func clampU8(v int) uint8 {
-	if v < 0 {
-		return 0
-	} else if v > 255 {
-		return 255
-	}
-	return uint8(v)
-}
-
-func processTest(testName string, imgUrls []string, output string) (bool, error) {
-	if strings.ContainsRune(testName, '/') {
-		return false, 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 false, err
-		}
-		img, err := png.Decode(resp.Body)
-		resp.Body.Close()
-		if err != nil {
-			return false, 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 false, 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 false, nil
-	}
-
-	if err := os.Mkdir(output_directory, os.ModePerm); err != nil && !os.IsExist(err) {
-		return false, err
-	}
-	if err := writePngToFile(path.Join(output_directory, min_png), &img_min); err != nil {
-		return false, err
-	}
-	if err := writePngToFile(path.Join(output_directory, max_png), &img_max); err != nil {
-		return false, err
-	}
-	return true, nil
-}
-
-type LockedStringList struct {
-	List []string
-	mux sync.Mutex
-}
-
-func (l *LockedStringList) add(v string) {
-	l.mux.Lock()
-	defer l.mux.Unlock()
-	l.List = append(l.List, v)
-}
-
-
-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]
-	// output is removed and replaced with a clean directory.
-	if err := os.RemoveAll(output); err != nil && !os.IsNotExist(err) {
-		log.Fatal(err)
-	}
-	if err := os.MkdirAll(output, os.ModePerm); err != nil && !os.IsExist(err) {
-		log.Fatal(err)
-	}
-
-	records, err := readMetaJsonFile(input)
-	if err != nil {
-		log.Fatal(err)
-	}
-	sort.Sort(ExportTestRecordArray(records))
-
-	var results LockedStringList
-	var wg sync.WaitGroup
-	for _, record := range records {
-		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, results* LockedStringList) {
-			defer wg.Done()
-			success, err := processTest(testName, imgUrls, output)
-			if err != nil {
-				log.Fatal(err)
-			}
-			if success {
-				results.add(testName)
-			}
-			fmt.Printf("\r%-60s", testName)
-		}(record.TestName, goodUrls, output, &results)
-	}
-	wg.Wait()
-	fmt.Printf("\r%60s\n", "")
-	sort.Strings(results.List)
-	modelFile, err := os.Create(path.Join(output, "models.txt"))
-	if err != nil {
-		log.Fatal(err)
-	}
-	for _, v := range results.List {
-		fmt.Fprintln(modelFile, v)
-	}
-}
diff --git a/tools/skqp/make_skqp_model.cpp b/tools/skqp/make_skqp_model.cpp
new file mode 100644
index 0000000..4bf7e13
--- /dev/null
+++ b/tools/skqp/make_skqp_model.cpp
@@ -0,0 +1,82 @@
+// Copyright 2019 Google LLC.
+// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
+
+#include "SkBitmap.h"
+#include "SkCodec.h"
+#include "SkPngEncoder.h"
+#include "SkOSFile.h"
+
+static void update(SkBitmap* maxBitmap, SkBitmap* minBitmap, const SkBitmap& bm) {
+    SkASSERT(!bm.drawsNothing());
+    SkASSERT(4 == bm.bytesPerPixel());
+    if (maxBitmap->drawsNothing()) {
+        maxBitmap->allocPixels(bm.info());
+        maxBitmap->eraseColor(0x00000000);
+        minBitmap->allocPixels(bm.info());
+        minBitmap->eraseColor(0xFFFFFFFF);
+    }
+    SkASSERT_RELEASE(maxBitmap->info() == bm.info());
+    const SkPixmap& pmin = minBitmap->pixmap();
+    const SkPixmap& pmax = maxBitmap->pixmap();
+    const SkPixmap& pm = bm.pixmap();
+    for (int y = 0; y < pm.height(); ++y) {
+        for (int x = 0; x < pm.width(); ++x) {
+            uint32_t* minPtr = pmin.writable_addr32(x, y);
+            uint32_t* maxPtr = pmax.writable_addr32(x, y);
+            uint8_t minColor[4], maxColor[4], color[4];
+            memcpy(minColor, minPtr, 4);
+            memcpy(maxColor, maxPtr, 4);
+            memcpy(color, pm.addr32(x, y), 4);
+            for (unsigned i = 0; i < 4; ++i) {
+                minColor[i] = std::min(minColor[i], color[i]);
+                maxColor[i] = std::max(maxColor[i], color[i]);
+            }
+            memcpy(minPtr, minColor, 4);
+            memcpy(maxPtr, maxColor, 4);
+        }
+    }
+}
+
+static SkBitmap decode_to_srgb_8888_unpremul(const char* path) {
+    SkBitmap dst;
+    if (auto codec = SkCodec::MakeFromData(SkData::MakeFromFileName(path))) {
+        SkISize size = codec->getInfo().dimensions();
+        SkASSERT(!size.isEmpty());
+        dst.allocPixels(SkImageInfo::Make(
+                    size.width(), size.height(), kRGBA_8888_SkColorType,
+                    kUnpremul_SkAlphaType, SkColorSpace::MakeSRGB()));
+        if (SkCodec::kSuccess != codec->getPixels(dst.pixmap())) {
+            dst.reset();
+        }
+    }
+    return dst;
+}
+
+bool encode_png(const char* path, const SkPixmap& pixmap) {
+    if (!pixmap.addr()) {
+        return false;
+    }
+    SkPngEncoder::Options encOpts;
+    encOpts.fZLibLevel = 9;  // slow encode;
+    SkFILEWStream o(path);
+    return o.isValid() && SkPngEncoder::Encode(&o, pixmap, encOpts);
+}
+
+int main(int argc, char** argv) {
+    SkASSERT_RELEASE(argc > 2);
+    const char* src_dir = argv[1];
+    const char* dst_dir = argv[2];
+    SkBitmap maxBitmap, minBitmap;
+    SkOSFile::Iter iter(src_dir);
+    SkString name;
+    while (iter.next(&name)) {
+        name.prependf("%s/", src_dir);
+        SkBitmap bm = decode_to_srgb_8888_unpremul(name.c_str());
+        if (!bm.drawsNothing()) {
+            update(&maxBitmap, &minBitmap, bm);
+        }
+    }
+    SkASSERT_RELEASE(sk_mkdir(dst_dir));
+    encode_png(SkStringPrintf("%s/min.png", dst_dir).c_str(), minBitmap.pixmap());
+    encode_png(SkStringPrintf("%s/max.png", dst_dir).c_str(), maxBitmap.pixmap());
+}
diff --git a/tools/skqp/test_apk.sh b/tools/skqp/test_apk.sh
index 4a155d7..42a6382 100755
--- a/tools/skqp/test_apk.sh
+++ b/tools/skqp/test_apk.sh
@@ -34,7 +34,8 @@
 
 TDIR="$(mktemp -d "${TMPDIR:-/tmp}/skqp_report.XXXXXXXXXX")"
 
-adb install -r "$APK" || exit 2
+adb uninstall org.skia.skqp
+adb install "$APK" || exit 2
 adb logcat -c
 
 adb logcat TestRunner org.skia.skqp skia DEBUG "*:S" | tee "${TDIR}/logcat.txt" &
@@ -72,7 +73,7 @@
 if [ -f "$REPORT" ]; then
     grep 'f(.*;' "$REPORT"
     echo "$REPORT"
-    "$(dirname "$0")"/../../bin/sysopen "$REPORT"
+    "$(dirname "$0")"/../../bin/sysopen "$REPORT" > /dev/null 2>&1 &
 else
     echo "$TDIR"
 fi