Refactored some test code and tweaked SVG output

- Moves more file IO code into shared helpers
- Fixes a scale bug with plots of parametric curves
- Split up SVG code into several functions, uses
  groups to isolate transforms
- Adds approximate TF plot over the TRC table plots
- Adds gamut diagram output

Change-Id: Ifad7049ae97e39514bd41a79f996463fcddf32b1
Reviewed-on: https://skia-review.googlesource.com/111482
Commit-Queue: Brian Osman <brianosman@google.com>
Reviewed-by: Mike Klein <mtklein@google.com>
diff --git a/iccdump.c b/iccdump.c
index f07885e..57bb0b8 100644
--- a/iccdump.c
+++ b/iccdump.c
@@ -27,6 +27,81 @@
     exit(1);
 }
 
+// xy co-ordinates of the CIE 1931 standard observer XYZ functions.
+// wavelength is sampled every 5 nm in [360, 700].
+// This is effectively the hull of the horseshoe in a chromaticity diagram.
+static const double kSpectralHull[] = {
+    0.17556, 0.00529384,
+    0.175161, 0.00525635,
+    0.174821, 0.0052206,
+    0.17451, 0.00518164,
+    0.174112, 0.00496373,
+    0.174008, 0.00498055,
+    0.173801, 0.00491541,
+    0.17356, 0.0049232,
+    0.173337, 0.00479674,
+    0.173021, 0.00477505,
+    0.172577, 0.0047993,
+    0.172087, 0.00483252,
+    0.171407, 0.00510217,
+    0.170301, 0.00578851,
+    0.168878, 0.00690024,
+    0.166895, 0.00855561,
+    0.164412, 0.0108576,
+    0.161105, 0.0137934,
+    0.156641, 0.0177048,
+    0.150985, 0.0227402,
+    0.14396, 0.029703,
+    0.135503, 0.0398791,
+    0.124118, 0.0578025,
+    0.109594, 0.0868425,
+    0.0912935, 0.132702,
+    0.0687059, 0.200723,
+    0.0453907, 0.294976,
+    0.0234599, 0.412703,
+    0.00816803, 0.538423,
+    0.00385852, 0.654823,
+    0.0138702, 0.750186,
+    0.0388518, 0.812016,
+    0.0743024, 0.833803,
+    0.114161, 0.826207,
+    0.154722, 0.805863,
+    0.192876, 0.781629,
+    0.22962, 0.754329,
+    0.265775, 0.724324,
+    0.301604, 0.692308,
+    0.337363, 0.658848,
+    0.373102, 0.624451,
+    0.408736, 0.589607,
+    0.444062, 0.554714,
+    0.478775, 0.520202,
+    0.512486, 0.486591,
+    0.544787, 0.454434,
+    0.575151, 0.424232,
+    0.602933, 0.396497,
+    0.627037, 0.372491,
+    0.648233, 0.351395,
+    0.665764, 0.334011,
+    0.680079, 0.319747,
+    0.691504, 0.308342,
+    0.700606, 0.299301,
+    0.707918, 0.292027,
+    0.714032, 0.285929,
+    0.719033, 0.280935,
+    0.723032, 0.276948,
+    0.725992, 0.274008,
+    0.728272, 0.271728,
+    0.729969, 0.270031,
+    0.731089, 0.268911,
+    0.731993, 0.268007,
+    0.732719, 0.267281,
+    0.733417, 0.266583,
+    0.734047, 0.265953,
+    0.73439, 0.26561,
+    0.734592, 0.265408,
+    0.73469, 0.26531,
+};
+
 static uint16_t read_big_u16(const uint8_t* ptr) {
     uint16_t be;
     memcpy(&be, ptr, sizeof(be));
@@ -45,61 +120,86 @@
 static const double kSVGScaleX = 800.0;
 static const double kSVGScaleY = 800.0;
 
-static double svg_map_x(double x) {
-    return x * kSVGScaleX + kSVGMarginLeft;
-}
+static const char* kSVG_RGB_Colors[3] = { "red", "green", "blue" };
+static const char* kSVG_CMYK_Colors[4] = { "cyan", "magenta", "yellow", "black" };
 
-static double svg_map_y(double y) {
-    return (1.0 - y) * kSVGScaleY + kSVGMarginTop;
-}
-
-static void dump_curves_svg(const char* name, uint32_t num_curves, const skcms_Curve* curves) {
-    char filename[256];
-    if (snprintf(filename, sizeof(filename), "%s.svg", name) < 0) {
-        return;
-    }
+static FILE* svg_open(const char* filename) {
     FILE* fp = fopen(filename, "wb");
     if (!fp) {
+        fatal("Unable to open output file");
+    }
+
+    fprintf(fp, "<svg width=\"%g\" height=\"%g\" xmlns=\"http://www.w3.org/2000/svg\">\n",
+            kSVGMarginLeft + kSVGScaleX + kSVGMarginRight,
+            kSVGMarginTop + kSVGScaleY + kSVGMarginBottom);
+    return fp;
+}
+
+static void svg_close(FILE* fp) {
+    fprintf(fp, "</svg>\n");
+    fclose(fp);
+}
+
+#define svg_push_group(fp, fmt, ...) fprintf(fp, "<g " fmt ">\n", __VA_ARGS__)
+
+static void svg_pop_group(FILE* fp) {
+    fprintf(fp, "</g>\n");
+}
+
+static void svg_axes(FILE* fp) {
+    fprintf(fp, "<polyline fill=\"none\" stroke=\"black\" vector-effect=\"non-scaling-stroke\" "
+                "points=\"0,1 0,0 1,0\"/>\n");
+}
+
+static void svg_transfer_function(FILE* fp, const skcms_TransferFunction* tf, const char* color) {
+    fprintf(fp, "<polyline fill=\"none\" stroke=\"%s\" vector-effect=\"non-scaling-stroke\" "
+            "points=\"\n", color);
+
+    for (int i = 0; i < 256; ++i) {
+        float x = i / 255.0f;
+        float t = skcms_TransferFunction_eval(tf, x);
+        fprintf(fp, "%g, %g\n", (double)x, (double)t);
+    }
+    fprintf(fp, "\"/>\n");
+}
+
+static void svg_curve(FILE* fp, const skcms_Curve* curve, const char* color) {
+    if (!curve->table_entries) {
+        svg_transfer_function(fp, &curve->parametric, color);
         return;
     }
 
-    fprintf(fp, "<svg width=\"%f\" height=\"%f\" xmlns=\"http://www.w3.org/2000/svg\">\n",
-            kSVGMarginLeft + kSVGScaleX + kSVGMarginRight,
-            kSVGMarginTop + kSVGScaleY + kSVGMarginBottom);
-    // Axes
-    fprintf(fp, "<polyline fill=\"none\" stroke=\"black\" points=\"%f,%f %f,%f %f,%f\"/>\n",
-            svg_map_x(0), svg_map_y(1), svg_map_x(0), svg_map_y(0), svg_map_x(1), svg_map_y(0));
+    double xScale = 1.0 / (curve->table_entries - 1.0);
+    double yScale = curve->table_8 ? (1.0 / 255) : (1.0 / 65535);
+    fprintf(fp, "<polyline fill=\"none\" stroke=\"%s\" vector-effect=\"non-scaling-stroke\" "
+            "transform=\"scale(%g %g)\" points=\"\n",
+            color, xScale, yScale);
 
-    // Curves
-    const char* rgb_colors[3] = { "red", "green", "blue" };
-    const char* cmyk_colors[4] = { "cyan", "magenta", "yellow", "black" };
-    const char** colors = (num_curves == 3) ? rgb_colors : cmyk_colors;
-    for (uint32_t c = 0; c < num_curves; ++c) {
-        uint32_t num_entries = curves[c].table_entries ? curves[c].table_entries : 256;
-        double yScale = curves[c].table_8 ? (1.0 / 255) : curves[c].table_16 ? (1.0 / 65535) : 1.0;
-
-        fprintf(fp, "<polyline fill=\"none\" stroke=\"%s\" vector-effect=\"non-scaling-stroke\" "
-                    "transform=\"matrix(%f 0 0 %f %f %f)\" points=\"\n",
-                colors[c],
-                kSVGScaleX / (num_entries - 1.0), -kSVGScaleY * yScale,
-                kSVGMarginLeft, kSVGScaleY + kSVGMarginTop);
-
-        for (uint32_t i = 0; i < num_entries; ++i) {
-            if (curves[c].table_8) {
-                fprintf(fp, "%3u, %3u\n", i, curves[c].table_8[i]);
-            } else if (curves[c].table_16) {
-                fprintf(fp, "%4u, %5u\n", i, read_big_u16(curves[c].table_16 + 2 * i));
-            } else {
-                double x = i / (num_entries - 1.0);
-                double t = (double)skcms_TransferFunction_eval(&curves[c].parametric, (float)x);
-                fprintf(fp, "%f, %f\n", x, t);
-            }
+    for (uint32_t i = 0; i < curve->table_entries; ++i) {
+        if (curve->table_8) {
+            fprintf(fp, "%3u, %3u\n", i, curve->table_8[i]);
+        } else {
+            fprintf(fp, "%4u, %5u\n", i, read_big_u16(curve->table_16 + 2 * i));
         }
-        fprintf(fp, "\"/>\n");
     }
+    fprintf(fp, "\"/>\n");
+}
 
-    fprintf(fp, "</svg>\n");
-    fclose(fp);
+static void svg_curves(FILE* fp, uint32_t num_curves, const skcms_Curve* curves,
+                       const char** colors) {
+    for (uint32_t c = 0; c < num_curves; ++c) {
+        svg_curve(fp, curves + c, colors[c]);
+    }
+}
+
+static void dump_curves_svg(const char* filename, uint32_t num_curves, const skcms_Curve* curves) {
+    FILE* fp = svg_open(filename);
+    svg_push_group(fp, "transform=\"translate(%g %g) scale(%g %g)\"",
+                   kSVGMarginLeft, kSVGMarginTop + kSVGScaleY, kSVGScaleX, -kSVGScaleY);
+    svg_axes(fp);
+    svg_curves(fp, num_curves, curves, (num_curves == 3) ? kSVG_RGB_Colors : kSVG_CMYK_Colors);
+    svg_pop_group(fp);
+    svg_close(fp);
 }
 
 int main(int argc, char** argv) {
@@ -119,27 +219,10 @@
         return 1;
     }
 
-    FILE* fp = fopen(filename, "rb");
-    if (!fp) {
-        fatal("Unable to open input file");
-    }
-
-    fseek(fp, 0L, SEEK_END);
-    long slen = ftell(fp);
-    if (slen <= 0) {
-        fatal("ftell failed");
-    }
-    size_t len = (size_t)slen;
-    rewind(fp);
-
-    void* buf = malloc(len);
-    if (!buf) {
-        fatal("malloc failed");
-    }
-    size_t bytesRead = fread(buf, 1, len, fp);
-    fclose(fp);
-    if (bytesRead != len) {
-        fatal("Unable to read file");
+    void* buf = NULL;
+    size_t len = 0;
+    if (!load_file(filename, &buf, &len)) {
+        fatal("Unable to load input file");
     }
 
     skcms_ICCProfile profile;
@@ -150,21 +233,59 @@
     dump_profile(&profile, stdout, false);
 
     if (svg) {
+        if (profile.has_toXYZD50) {
+            FILE* fp = svg_open("gamut.svg");
+            svg_push_group(fp, "transform=\"translate(%g %g) scale(%g %g)\"",
+                           kSVGMarginLeft, kSVGMarginTop + kSVGScaleY, kSVGScaleX, -kSVGScaleY);
+            svg_axes(fp);
+
+            fprintf(fp, "<polygon fill=\"none\" stroke=\"black\" "
+                    "vector-effect=\"non-scaling-stroke\" points=\"\n");
+            for (int i = 0; i < ARRAY_COUNT(kSpectralHull); i += 2) {
+                fprintf(fp, "%g, %g\n", kSpectralHull[i], kSpectralHull[i + 1]);
+            }
+            fprintf(fp, "\"/>\n");
+
+            const skcms_Matrix3x3* m = &profile.toXYZD50;
+            float rSum = m->vals[0][0] + m->vals[1][0] + m->vals[2][0];
+            float gSum = m->vals[0][1] + m->vals[1][1] + m->vals[2][1];
+            float bSum = m->vals[0][2] + m->vals[1][2] + m->vals[2][2];
+            fprintf(fp, "<polygon fill=\"none\" stroke=\"black\" "
+                    "vector-effect=\"non-scaling-stroke\" points=\"%g,%g %g,%g %g,%g\"/>\n",
+                    (double)(m->vals[0][0] / rSum), (double)(m->vals[1][0] / rSum),
+                    (double)(m->vals[0][1] / gSum), (double)(m->vals[1][1] / gSum),
+                    (double)(m->vals[0][2] / bSum), (double)(m->vals[1][2] / bSum));
+
+            svg_pop_group(fp);
+            svg_close(fp);
+        }
+
         if (profile.has_trc) {
-            dump_curves_svg("TRC_curves", 3, profile.trc);
+            FILE* fp = svg_open("TRC_curves.svg");
+            svg_push_group(fp, "transform=\"translate(%g %g) scale(%g %g)\"",
+                           kSVGMarginLeft, kSVGMarginTop + kSVGScaleY, kSVGScaleX, -kSVGScaleY);
+            svg_axes(fp);
+            svg_curves(fp, 3, profile.trc, kSVG_RGB_Colors);
+            skcms_TransferFunction approx;
+            float max_error;
+            if (skcms_ApproximateTransferFunction(&profile, &approx, &max_error)) {
+                svg_transfer_function(fp, &approx, "magenta");
+            }
+            svg_pop_group(fp);
+            svg_close(fp);
         }
 
         if (profile.has_A2B) {
             const skcms_A2B* a2b = &profile.A2B;
             if (a2b->input_channels) {
-                dump_curves_svg("A_curves", a2b->input_channels, a2b->input_curves);
+                dump_curves_svg("A_curves.svg", a2b->input_channels, a2b->input_curves);
             }
 
             if (a2b->matrix_channels) {
-                dump_curves_svg("M_curves", a2b->matrix_channels, a2b->matrix_curves);
+                dump_curves_svg("M_curves.svg", a2b->matrix_channels, a2b->matrix_curves);
             }
 
-            dump_curves_svg("B_curves", a2b->output_channels, a2b->output_curves);
+            dump_curves_svg("B_curves.svg", a2b->output_channels, a2b->output_curves);
         }
     }
 
diff --git a/test_only.c b/test_only.c
index 39ad27e..96c4d50 100644
--- a/test_only.c
+++ b/test_only.c
@@ -12,6 +12,7 @@
 #include "skcms.h"
 #include "test_only.h"
 #include "src/TransferFunction.h"
+#include <stdlib.h>
 
 static void signature_to_string(uint32_t sig, char* str) {
     str[0] = (char)((sig >> 24) & 0xFF);
@@ -172,3 +173,46 @@
         }
     }
 }
+
+bool load_file_fp(FILE* fp, void** buf, size_t* len) {
+    if (fseek(fp, 0L, SEEK_END) != 0) {
+        return false;
+    }
+    long size = ftell(fp);
+    if (size <= 0) {
+        return false;
+    }
+    *len = (size_t)size;
+    rewind(fp);
+
+    *buf = malloc(*len);
+    if (!*buf) {
+        return false;
+    }
+
+    if (fread(*buf, 1, *len, fp) != *len) {
+        free(*buf);
+        return false;
+    }
+    return true;
+}
+
+bool load_file(const char* filename, void** buf, size_t* len) {
+    FILE* fp = fopen(filename, "rb");
+    if (!fp) {
+        return false;
+    }
+    bool result = load_file_fp(fp, buf, len);
+    fclose(fp);
+    return result;
+}
+
+bool write_file(const char* filename, void* buf, size_t len) {
+    FILE* fp = fopen(filename, "wb");
+    if (!fp) {
+        return false;
+    }
+    bool result = (fwrite(buf, 1, len, fp) == len);
+    fclose(fp);
+    return result;
+}
diff --git a/test_only.h b/test_only.h
index 6bee2d9..a30d874 100644
--- a/test_only.h
+++ b/test_only.h
@@ -11,3 +11,8 @@
 #include <stdio.h>
 
 void dump_profile(const skcms_ICCProfile* profile, FILE* fp, bool for_unit_test);
+
+bool load_file_fp(FILE* fp, void** buf, size_t* len);
+bool load_file(const char* filename, void** buf, size_t* len);
+
+bool write_file(const char* filename, void* buf, size_t len);
diff --git a/tests.c b/tests.c
index abac43b..0034238 100644
--- a/tests.c
+++ b/tests.c
@@ -526,33 +526,6 @@
     { "profiles/fuzz/a2b_too_many_input_channels.icc", NULL }, // oss-fuzz:6521
 };
 
-static void load_file_fp(FILE* fp, void** buf, size_t* len) {
-    expect(fseek(fp, 0L, SEEK_END) == 0);
-    long size = ftell(fp);
-    expect(size > 0);
-    *len = (size_t)size;
-    rewind(fp);
-
-    *buf = malloc(*len);
-    expect(*buf);
-
-    expect(fread(*buf, 1, *len, fp) == *len);
-}
-
-static void load_file(const char* filename, void** buf, size_t* len) {
-    FILE* fp = fopen(filename, "rb");
-    expect(fp);
-    load_file_fp(fp, buf, len);
-    fclose(fp);
-}
-
-static void write_file(const char* filename, void* buf, size_t len) {
-    FILE* fp = fopen(filename, "wb");
-    expect(fp);
-    expect(fwrite(buf, 1, len, fp) == len);
-    fclose(fp);
-}
-
 static void check_roundtrip_transfer_functions(const skcms_TransferFunction* fwd,
                                                const skcms_TransferFunction* rev,
                                                float tol) {
@@ -570,7 +543,7 @@
 
         void* buf = NULL;
         size_t len = 0;
-        load_file(test->filename, &buf, &len);
+        expect(load_file(test->filename, &buf, &len));
         skcms_ICCProfile profile;
         bool parsed = skcms_Parse(buf, len, &profile);
 
@@ -585,7 +558,7 @@
 
         void* dump_buf = NULL;
         size_t dump_len = 0;
-        load_file_fp(dump, &dump_buf, &dump_len);
+        expect(load_file_fp(dump, &dump_buf, &dump_len));
         fclose(dump);
 
         char ref_filename[256];
@@ -595,12 +568,12 @@
 
         if (regen) {
             // Just write out new test data if in regen mode
-            write_file(ref_filename, dump_buf, dump_len);
+            expect(write_file(ref_filename, dump_buf, dump_len));
         } else {
             // Read in existing test data
             void* ref_buf = NULL;
             size_t ref_len = 0;
-            load_file(ref_filename, &ref_buf, &ref_len);
+            expect(load_file(ref_filename, &ref_buf, &ref_len));
 
             if (dump_len != ref_len || memcmp(dump_buf, ref_buf, dump_len) != 0) {
                 // Write out the new data on a mismatch
@@ -830,7 +803,7 @@
     // We'll test that parametric sRGB roundtrips with itself, bytes -> bytes.
     void*  srgb_ptr;
     size_t srgb_len;
-    load_file("profiles/mobile/sRGB_parametric.icc", &srgb_ptr, &srgb_len);
+    expect(load_file("profiles/mobile/sRGB_parametric.icc", &srgb_ptr, &srgb_len));
 
     skcms_ICCProfile srgbA, srgbB;
     expect(skcms_Parse(srgb_ptr, srgb_len, &srgbA));
@@ -879,20 +852,20 @@
 static void test_FloatRoundTrips() {
     void*  srgb_ptr;
     size_t srgb_len;
-    load_file("profiles/mobile/sRGB_parametric.icc", &srgb_ptr, &srgb_len);
+    expect(load_file("profiles/mobile/sRGB_parametric.icc", &srgb_ptr, &srgb_len));
 
 
     void*  dp3_ptr;
     size_t dp3_len;
-    load_file("profiles/mobile/Display_P3_parametric.icc", &dp3_ptr, &dp3_len);
+    expect(load_file("profiles/mobile/Display_P3_parametric.icc", &dp3_ptr, &dp3_len));
 
     void*  ll_ptr;
     size_t ll_len;
-    load_file("profiles/color.org/Lower_Left.icc", &ll_ptr, &ll_len);
+    expect(load_file("profiles/color.org/Lower_Left.icc", &ll_ptr, &ll_len));
 
     void*  lr_ptr;
     size_t lr_len;
-    load_file("profiles/color.org/Lower_Right.icc", &lr_ptr, &lr_len);
+    expect(load_file("profiles/color.org/Lower_Right.icc", &lr_ptr, &lr_len));
 
     skcms_ICCProfile srgb, dp3, ll, lr;
     expect(skcms_Parse(srgb_ptr, srgb_len, &srgb));
@@ -918,16 +891,16 @@
     size_t len;
     skcms_ICCProfile p;
 
-    load_file("profiles/mobile/sRGB_parametric.icc", &ptr, &len);
+    expect( load_file("profiles/mobile/sRGB_parametric.icc", &ptr, &len) );
     expect( skcms_Parse(ptr, len, &p) && p.has_tf && skcms_IsSRGB(&p.tf) );
     free(ptr);
 
-    load_file("profiles/mobile/Display_P3_parametric.icc", &ptr, &len);
+    expect( load_file("profiles/mobile/Display_P3_parametric.icc", &ptr, &len) );
     expect( skcms_Parse(ptr, len, &p) && p.has_tf && skcms_IsSRGB(&p.tf) );
     free(ptr);
 
     // TODO: relax skcms_IsSRGB() so that this one is seen as sRGB too?  It's not far.
-    load_file("profiles/mobile/iPhone7p.icc", &ptr, &len);
+    expect( load_file("profiles/mobile/iPhone7p.icc", &ptr, &len) );
     expect( skcms_Parse(ptr, len, &p) && p.has_tf && !skcms_IsSRGB(&p.tf) );
     free(ptr);
 }
@@ -938,7 +911,7 @@
     void* ptr;
     size_t len;
     skcms_ICCProfile sRGB;
-    load_file("profiles/mobile/sRGB_parametric.icc", &ptr, &len);
+    expect( load_file("profiles/mobile/sRGB_parametric.icc", &ptr, &len) );
     expect( skcms_Parse(ptr, len, &sRGB) );
 
     skcms_ICCProfile linear_sRGB = sRGB;