add a color management page to skia.org

preview at https://skia.org/user/color?cl=279061

Change-Id: I66dd7911593fc9f8d3864f4829cc2df26923f5d8
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/279061
Reviewed-by: Brian Osman <brianosman@google.com>
Commit-Queue: Mike Klein <mtklein@google.com>
diff --git a/site/user/color.md b/site/user/color.md
new file mode 100644
index 0000000..b6bc59d
--- /dev/null
+++ b/site/user/color.md
@@ -0,0 +1,85 @@
+Skia Color Management
+=====================
+
+What we mean by color management
+--------------------------------
+
+All the color spaces Skia works with describe themselves by how to transform
+colors from that color space to a common "connection" color space called XYZ
+D50.  And we can infer from that same description how to transform from that
+XYZ D50 space back to the original color space.  XYZ D50 is a color space
+represented in three dimensions like RGB, but the XYZ parts are not RGB-like at
+all, rather a linear remix of those channels.  Y is closest to what you'd think
+of as brightness, but X and Z are a little more abstract.  It's kind of like
+YUV if you're familiar with that.  The "D50" part refers to the whitepoint of
+this space, around 5000 Kelvin.
+
+All color managed drawing is divided into six parts, three steps connecting the
+source colors to that XYZ D50 space, then three symmetric steps connecting back
+from XYZ D50 to the destination color space.  Some of these steps can
+annihilate with each other into no-ops, sometimes all the way to the entire
+process amounting to a no-op when the source space and destination space are
+the same.  Here are the steps:
+
+Color management steps
+----------------------
+
+1. unpremultiply if the source color is premultiplied  -- alpha is not involved
+   in color management, and we need to divide it out if it's multiplied in
+2. linearize the source color using the source color space's transfer function
+3. convert those unpremultiplied, linear source colors to XYZ D50 gamut by
+   multiplying by a 3x3 matrix
+4. convert those XYZ D50 colors to the destination gamut by multiplying by a 3x3 matrix
+5. encode that color using the inverse of the destination color space's transfer function
+6. premultiply by alpha if the destination is premultiplied
+
+If you poke around in our code the clearest place to see this logic is in a
+type called SkColorSpaceXformSteps.  You'll see it as 5 steps there: we always
+merge the innermost two operations into a single 3x3 matrix multiply.
+
+Optimizations
+-------------
+
+Whenever we're about to do some drawing we look at which of those steps we
+really need to do.  Any step that's a fundamental no-op we skip:
+
+   * skip 1 if the source is already unpremultiplied
+   * skip 2 if the source is already linearly encoded
+   * skip 3 and 4 if that single concatenated matrix is identity (i.e. the
+     source and destination color spaces have the same gamut)
+   * skip 5 if the destination wants linear encoding
+   * skip 6 if the destination wants to be unpremultiplied
+
+We can reason from those basic skips into some more advanced optimizations:
+
+  * if we've skipped 3 and 4 already, we can skip 2 and 5 any time the transfer
+    functions are the same  -- sending colors through a given transfer function
+    and its own inverse is a no-op
+  * if we've skipped all of 2-5, we can skip 1 and 6 if we were going to do
+    both --- no sense in unpremultiplying just to re-premultiply.
+  * opaque colors can be treated as either unpremultiplied or premultiplied,
+    whichever lets us skip more steps.
+
+All this comes together to an impressive "nothing to do" most of the time.  If
+you're drawing opaque colors in a given color space to a destination tagged
+with that same color space, we'll notice we can skip all six steps.  Sometimes
+fewer steps are needed, sometimes more.  In general if you need to do a gamut
+conversion, you should generally expect all the middle steps to be active.
+Steps 2 and 5 are by far the most expensive to compute.
+
+nullptr SkColorSpace defaults
+-----------------------------
+
+Now how do nullptr SkColorSpace defaults work into all of this?  We preface all
+that logic I've just mentioned above with this little snippet:
+
+     if (srcCS == nullptr) { srcCS = sRGB; }
+     if (dstCS == nullptr) { dstCS = srcCS; }
+
+(Order matters there.)  The gist is, we assume any untagged sources are sRGB.
+And if you leave your surface untagged, we act as if your destination fluidly
+matches whatever source you're trying to draw into it, which skips at least
+steps 2-5 as listed above, maintaining an unmanaged color mode of drawing
+compatible with how retro Skia used to work before we introduce color
+management.  It's not very principled, but it's handy in practice to keep
+around.
diff --git a/src/core/SkColorSpaceXformSteps.cpp b/src/core/SkColorSpaceXformSteps.cpp
index 294916f..7bc23c1 100644
--- a/src/core/SkColorSpaceXformSteps.cpp
+++ b/src/core/SkColorSpaceXformSteps.cpp
@@ -11,7 +11,7 @@
 #include "src/core/SkRasterPipeline.h"
 #include "src/core/SkVM.h"
 
-// TODO(mtklein): explain the logic of this file
+// See skia.org/user/color  (== site/user/color.md).
 
 SkColorSpaceXformSteps::SkColorSpaceXformSteps(SkColorSpace* src, SkAlphaType srcAT,
                                                SkColorSpace* dst, SkAlphaType dstAT) {