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) {