Home | History | Annotate | Download | only in tools
      1 /*
      2  * Copyright 2016 Google Inc.
      3  *
      4  * Use of this source code is governed by a BSD-style license that can be
      5  * found in the LICENSE file.
      6  */
      7 
      8 #include "Resources.h"
      9 
     10 #include "SkBitmap.h"
     11 #include "SkCanvas.h"
     12 #include "SkCodec.h"
     13 #include "SkColorSpace_A2B.h"
     14 #include "SkColorSpace_XYZ.h"
     15 #include "SkColorSpacePriv.h"
     16 #include "SkCommandLineFlags.h"
     17 #include "SkImageEncoder.h"
     18 #include "SkMatrix44.h"
     19 #include "SkOSFile.h"
     20 
     21 #include "sk_tool_utils.h"
     22 
     23 DEFINE_string(input, "input.png", "A path to the input image or icc profile.");
     24 DEFINE_string(gamut_output, "gamut_output.png", "A path to the output gamut image.");
     25 DEFINE_string(gamma_output, "gamma_output.png", "A path to the output gamma image.");
     26 DEFINE_bool(sRGB_gamut, false, "Draws the sRGB gamut on the gamut visualization.");
     27 DEFINE_bool(adobeRGB, false, "Draws the Adobe RGB gamut on the gamut visualization.");
     28 DEFINE_bool(sRGB_gamma, false, "Draws the sRGB gamma on all gamma output images.");
     29 DEFINE_string(uncorrected, "", "A path to reencode the uncorrected input image.");
     30 
     31 static const char* kRGBChannelNames[3] = {
     32     "Red  ", "Green", "Blue "
     33 };
     34 
     35 static const SkColor kRGBChannelColors[3] = {
     36     SkColorSetARGB(164, 255, 32, 32),
     37     SkColorSetARGB(164, 32, 255, 32),
     38     SkColorSetARGB(164, 32, 32, 255)
     39 };
     40 
     41 static void dump_transfer_fn(SkGammaNamed gammaNamed) {
     42     switch (gammaNamed) {
     43         case kSRGB_SkGammaNamed:
     44             SkDebugf("Transfer Function: sRGB\n");
     45             return;
     46         case k2Dot2Curve_SkGammaNamed:
     47             SkDebugf("Exponential Transfer Function: Exponent 2.2\n");
     48             return;
     49         case kLinear_SkGammaNamed:
     50             SkDebugf("Transfer Function: Linear\n");
     51             return;
     52         default:
     53             break;
     54     }
     55 
     56 }
     57 
     58 static void dump_transfer_fn(const SkGammas& gammas) {
     59     SkASSERT(gammas.channels() == 3);
     60     for (int i = 0; i < gammas.channels(); i++) {
     61         if (gammas.isNamed(i)) {
     62             switch (gammas.data(i).fNamed) {
     63                 case kSRGB_SkGammaNamed:
     64                     SkDebugf("%s Transfer Function: sRGB\n", kRGBChannelNames[i]);
     65                     return;
     66                 case k2Dot2Curve_SkGammaNamed:
     67                     SkDebugf("%s Transfer Function: Exponent 2.2\n", kRGBChannelNames[i]);
     68                     return;
     69                 case kLinear_SkGammaNamed:
     70                     SkDebugf("%s Transfer Function: Linear\n", kRGBChannelNames[i]);
     71                     return;
     72                 default:
     73                     SkASSERT(false);
     74                     continue;
     75             }
     76         } else if (gammas.isValue(i)) {
     77             SkDebugf("%s Transfer Function: Exponent %.3f\n", kRGBChannelNames[i],
     78                      gammas.data(i).fValue);
     79         } else if (gammas.isParametric(i)) {
     80             const SkColorSpaceTransferFn& fn = gammas.data(i).params(&gammas);
     81             SkDebugf("%s Transfer Function: Parametric A = %.3f, B = %.3f, C = %.3f, D = %.3f, "
     82                      "E = %.3f, F = %.3f, G = %.3f\n", kRGBChannelNames[i], fn.fA, fn.fB, fn.fC,
     83                      fn.fD, fn.fE, fn.fF, fn.fG);
     84         } else {
     85             SkASSERT(gammas.isTable(i));
     86             SkDebugf("%s Transfer Function: Table (%d entries)\n", kRGBChannelNames[i],
     87                     gammas.data(i).fTable.fSize);
     88         }
     89     }
     90 }
     91 
     92 static inline float parametric(const SkColorSpaceTransferFn& fn, float x) {
     93     return x >= fn.fD ? powf(fn.fA*x + fn.fB, fn.fG) + fn.fE
     94                       : fn.fC*x + fn.fF;
     95 }
     96 
     97 static void draw_transfer_fn(SkCanvas* canvas, SkGammaNamed gammaNamed, const SkGammas* gammas,
     98                              SkColor color, int col) {
     99     SkColorSpaceTransferFn fn[4];
    100     struct TableInfo {
    101         const float* fTable;
    102         int          fSize;
    103     };
    104     TableInfo table[4];
    105     bool isTable[4] = {false, false, false, false};
    106     const int channels = gammas ? gammas->channels() : 1;
    107     SkASSERT(channels <= 4);
    108     if (kNonStandard_SkGammaNamed != gammaNamed) {
    109         dump_transfer_fn(gammaNamed);
    110         for (int i = 0; i < channels; ++i) {
    111             named_to_parametric(&fn[i], gammaNamed);
    112         }
    113     } else {
    114         SkASSERT(gammas);
    115         dump_transfer_fn(*gammas);
    116         for (int i = 0; i < channels; ++i) {
    117             if (gammas->isTable(i)) {
    118                 table[i].fTable = gammas->table(i);
    119                 table[i].fSize = gammas->data(i).fTable.fSize;
    120                 isTable[i] = true;
    121             } else {
    122                 switch (gammas->type(i)) {
    123                     case SkGammas::Type::kNamed_Type:
    124                         named_to_parametric(&fn[i], gammas->data(i).fNamed);
    125                         break;
    126                     case SkGammas::Type::kValue_Type:
    127                         value_to_parametric(&fn[i], gammas->data(i).fValue);
    128                         break;
    129                     case SkGammas::Type::kParam_Type:
    130                         fn[i] = gammas->params(i);
    131                         break;
    132                     default:
    133                         SkASSERT(false);
    134                 }
    135             }
    136         }
    137     }
    138     SkPaint paint;
    139     paint.setStyle(SkPaint::kStroke_Style);
    140     paint.setColor(color);
    141     paint.setStrokeWidth(2.0f);
    142     // note: gamma has positive values going up in this image so this origin is
    143     //       the bottom left and we must subtract y instead of adding.
    144     const float gap         = 16.0f;
    145     const float cellWidth   = 500.0f;
    146     const float cellHeight  = 500.0f;
    147     const float gammaWidth  = cellWidth - 2 * gap;
    148     const float gammaHeight = cellHeight - 2 * gap;
    149     // gamma origin point
    150     const float ox = gap + cellWidth * col;
    151     const float oy = gap + gammaHeight;
    152     for (int i = 0; i < channels; ++i) {
    153         if (kNonStandard_SkGammaNamed == gammaNamed) {
    154             paint.setColor(kRGBChannelColors[i]);
    155         } else {
    156             paint.setColor(color);
    157         }
    158         if (isTable[i]) {
    159             auto tx = [&table,i](int index) {
    160                 return index / (table[i].fSize - 1.0f);
    161             };
    162             for (int ti = 1; ti < table[i].fSize; ++ti) {
    163                 canvas->drawLine(ox + gammaWidth * tx(ti - 1),
    164                                  oy - gammaHeight * table[i].fTable[ti - 1],
    165                                  ox + gammaWidth * tx(ti),
    166                                  oy - gammaHeight * table[i].fTable[ti],
    167                                  paint);
    168             }
    169         } else {
    170             const float step = 0.01f;
    171             float yPrev = parametric(fn[i], 0.0f);
    172             for (float x = step; x <= 1.0f; x += step) {
    173                 const float y = parametric(fn[i], x);
    174                 canvas->drawLine(ox + gammaWidth * (x - step), oy - gammaHeight * yPrev,
    175                                  ox + gammaWidth * x, oy - gammaHeight * y,
    176                                  paint);
    177                 yPrev = y;
    178             }
    179         }
    180     }
    181     paint.setColor(0xFF000000);
    182     paint.setStrokeWidth(3.0f);
    183     canvas->drawRect({ ox, oy - gammaHeight, ox + gammaWidth, oy }, paint);
    184 }
    185 
    186 /**
    187  *  Loads the triangular gamut as a set of three points.
    188  */
    189 static void load_gamut(SkPoint rgb[], const SkMatrix44& xyz) {
    190     // rx = rX / (rX + rY + rZ)
    191     // ry = rX / (rX + rY + rZ)
    192     // gx, gy, bx, and gy are calulcated similarly.
    193     float rSum = xyz.get(0, 0) + xyz.get(1, 0) + xyz.get(2, 0);
    194     float gSum = xyz.get(0, 1) + xyz.get(1, 1) + xyz.get(2, 1);
    195     float bSum = xyz.get(0, 2) + xyz.get(1, 2) + xyz.get(2, 2);
    196     rgb[0].fX = xyz.get(0, 0) / rSum;
    197     rgb[0].fY = xyz.get(1, 0) / rSum;
    198     rgb[1].fX = xyz.get(0, 1) / gSum;
    199     rgb[1].fY = xyz.get(1, 1) / gSum;
    200     rgb[2].fX = xyz.get(0, 2) / bSum;
    201     rgb[2].fY = xyz.get(1, 2) / bSum;
    202 }
    203 
    204 /**
    205  *  Calculates the area of the triangular gamut.
    206  */
    207 static float calculate_area(SkPoint abc[]) {
    208     SkPoint a = abc[0];
    209     SkPoint b = abc[1];
    210     SkPoint c = abc[2];
    211     return 0.5f * SkTAbs(a.fX*b.fY + b.fX*c.fY - a.fX*c.fY - c.fX*b.fY - b.fX*a.fY);
    212 }
    213 
    214 static void draw_gamut(SkCanvas* canvas, const SkMatrix44& xyz, const char* name, SkColor color,
    215                        bool label) {
    216     // Report the XYZ values.
    217     SkDebugf("%s\n", name);
    218     SkDebugf("       R     G     B\n");
    219     SkDebugf("X  %.3f %.3f %.3f\n", xyz.get(0, 0), xyz.get(0, 1), xyz.get(0, 2));
    220     SkDebugf("Y  %.3f %.3f %.3f\n", xyz.get(1, 0), xyz.get(1, 1), xyz.get(1, 2));
    221     SkDebugf("Z  %.3f %.3f %.3f\n", xyz.get(2, 0), xyz.get(2, 1), xyz.get(2, 2));
    222 
    223     // Calculate the points in the gamut from the XYZ values.
    224     SkPoint rgb[4];
    225     load_gamut(rgb, xyz);
    226 
    227     // Report the area of the gamut.
    228     SkDebugf("Area of Gamut: %.3f\n\n", calculate_area(rgb));
    229 
    230     // Magic constants that help us place the gamut triangles in the appropriate position
    231     // on the canvas.
    232     const float xScale = 2071.25f;  // Num pixels from 0 to 1 in x
    233     const float xOffset = 241.0f;   // Num pixels until start of x-axis
    234     const float yScale = 2067.78f;  // Num pixels from 0 to 1 in y
    235     const float yOffset = -144.78f; // Num pixels until start of y-axis
    236                                     // (negative because y extends beyond image bounds)
    237 
    238     // Now transform the points so they can be drawn on our canvas.
    239     // Note that y increases as we move down the canvas.
    240     rgb[0].fX = xOffset + xScale * rgb[0].fX;
    241     rgb[0].fY = yOffset + yScale * (1.0f - rgb[0].fY);
    242     rgb[1].fX = xOffset + xScale * rgb[1].fX;
    243     rgb[1].fY = yOffset + yScale * (1.0f - rgb[1].fY);
    244     rgb[2].fX = xOffset + xScale * rgb[2].fX;
    245     rgb[2].fY = yOffset + yScale * (1.0f - rgb[2].fY);
    246 
    247     // Repeat the first point to connect the polygon.
    248     rgb[3] = rgb[0];
    249     SkPaint paint;
    250     paint.setColor(color);
    251     paint.setStrokeWidth(6.0f);
    252     paint.setTextSize(75.0f);
    253     canvas->drawPoints(SkCanvas::kPolygon_PointMode, 4, rgb, paint);
    254     if (label) {
    255         canvas->drawText("R", 1, rgb[0].fX + 5.0f, rgb[0].fY + 75.0f, paint);
    256         canvas->drawText("G", 1, rgb[1].fX + 5.0f, rgb[1].fY - 5.0f, paint);
    257         canvas->drawText("B", 1, rgb[2].fX - 75.0f, rgb[2].fY - 5.0f, paint);
    258     }
    259 }
    260 
    261 int main(int argc, char** argv) {
    262     SkCommandLineFlags::SetUsage(
    263             "Usage: colorspaceinfo --input <path to input image or icc profile> "
    264                                   "--gamma_output <path to output gamma image> "
    265                                   "--gamut_output <path to output gamut image>"
    266                                   "--sRGB <draw canonical sRGB gamut> "
    267                                   "--adobeRGB <draw canonical Adobe RGB gamut> "
    268                                   "--uncorrected <path to reencoded, uncorrected input image>\n"
    269             "Description: Writes visualizations of the color space to the output image(s)  ."
    270                          "Also, if a path is provided, writes uncorrected bytes to an unmarked "
    271                          "png, for comparison with the input image.\n");
    272     SkCommandLineFlags::Parse(argc, argv);
    273     const char* input = FLAGS_input[0];
    274     const char* gamut_output = FLAGS_gamut_output[0];
    275     const char* gamma_output = FLAGS_gamma_output[0];
    276     if (!input || !gamut_output || !gamma_output) {
    277         SkCommandLineFlags::PrintUsage();
    278         return -1;
    279     }
    280 
    281     sk_sp<SkData> data(SkData::MakeFromFileName(input));
    282     if (!data) {
    283         SkDebugf("Cannot find input image.\n");
    284         return -1;
    285     }
    286     std::unique_ptr<SkCodec> codec(SkCodec::NewFromData(data));
    287     sk_sp<SkColorSpace> colorSpace = nullptr;
    288     const bool isImage = (codec != nullptr);
    289     if (isImage) {
    290         colorSpace = sk_ref_sp(codec->getInfo().colorSpace());
    291     } else {
    292         colorSpace = SkColorSpace::MakeICC(data->bytes(), data->size());
    293     }
    294 
    295     if (!colorSpace) {
    296         SkDebugf("Cannot create codec or icc profile from input file.\n");
    297         return -1;
    298     }
    299 
    300     // Load a graph of the CIE XYZ color gamut.
    301     SkBitmap gamutCanvasBitmap;
    302     if (!GetResourceAsBitmap("gamut.png", &gamutCanvasBitmap)) {
    303         SkDebugf("Program failure.\n");
    304         return -1;
    305     }
    306     SkCanvas gamutCanvas(gamutCanvasBitmap);
    307 
    308     SkBitmap gammaCanvasBitmap;
    309     gammaCanvasBitmap.allocN32Pixels(500, 500);
    310     SkCanvas gammaCanvas(gammaCanvasBitmap);
    311 
    312     // Draw the sRGB gamut if requested.
    313     if (FLAGS_sRGB_gamut) {
    314         sk_sp<SkColorSpace> sRGBSpace = SkColorSpace::MakeSRGB();
    315         const SkMatrix44* mat = as_CSB(sRGBSpace)->toXYZD50();
    316         SkASSERT(mat);
    317         draw_gamut(&gamutCanvas, *mat, "sRGB", 0xFFFF9394, false);
    318     }
    319 
    320     // Draw the Adobe RGB gamut if requested.
    321     if (FLAGS_adobeRGB) {
    322         sk_sp<SkColorSpace> adobeRGBSpace =
    323                 SkColorSpace_Base::MakeNamed(SkColorSpace_Base::kAdobeRGB_Named);
    324         const SkMatrix44* mat = as_CSB(adobeRGBSpace)->toXYZD50();
    325         SkASSERT(mat);
    326         draw_gamut(&gamutCanvas, *mat, "Adobe RGB", 0xFF31a9e1, false);
    327     }
    328 
    329     int gammaCol = 0;
    330     if (SkColorSpace_Base::Type::kXYZ == as_CSB(colorSpace)->type()) {
    331         const SkMatrix44* mat = as_CSB(colorSpace)->toXYZD50();
    332         SkASSERT(mat);
    333         auto xyz = static_cast<SkColorSpace_XYZ*>(colorSpace.get());
    334         draw_gamut(&gamutCanvas, *mat, input, 0xFF000000, true);
    335         if (FLAGS_sRGB_gamma) {
    336             draw_transfer_fn(&gammaCanvas, kSRGB_SkGammaNamed, nullptr, 0xFFFF9394, gammaCol);
    337         }
    338         draw_transfer_fn(&gammaCanvas, xyz->gammaNamed(), xyz->gammas(), 0xFF000000, gammaCol++);
    339     } else {
    340         SkDebugf("Color space is defined using an A2B tag.  It cannot be represented by "
    341                  "a transfer function and to D50 matrix.\n");
    342         return -1;
    343     }
    344 
    345     // marker to tell the web-tool the names of all images output
    346     SkDebugf("=========\n");
    347     auto saveCanvasBitmap = [](const SkBitmap& bitmap, const char *fname) {
    348         // Finally, encode the result to the output file.
    349         sk_sp<SkData> out = sk_tool_utils::EncodeImageToData(bitmap, SkEncodedImageFormat::kPNG,
    350                                                              100);
    351         if (!out) {
    352             SkDebugf("Failed to encode %s output.\n", fname);
    353             return false;
    354         }
    355         SkFILEWStream stream(fname);
    356         if (!stream.write(out->data(), out->size())) {
    357             SkDebugf("Failed to write %s output.\n", fname);
    358             return false;
    359         }
    360         // record name of canvas
    361         SkDebugf("%s\n", fname);
    362         return true;
    363     };
    364 
    365     // only XYZ images have a gamut visualization since the matrix in A2B is not
    366     // a gamut adjustment from RGB->XYZ always (or ever)
    367     if (SkColorSpace_Base::Type::kXYZ == as_CSB(colorSpace)->type() &&
    368         !saveCanvasBitmap(gamutCanvasBitmap, gamut_output)) {
    369         return -1;
    370     }
    371     if (gammaCol > 0 && !saveCanvasBitmap(gammaCanvasBitmap, gamma_output)) {
    372         return -1;
    373     }
    374 
    375     if (isImage) {
    376         SkDebugf("%s\n", input);
    377     }
    378     // Also, if requested, decode and reencode the uncorrected input image.
    379     if (!FLAGS_uncorrected.isEmpty() && isImage) {
    380         SkBitmap bitmap;
    381         int width = codec->getInfo().width();
    382         int height = codec->getInfo().height();
    383         bitmap.allocN32Pixels(width, height, kOpaque_SkAlphaType == codec->getInfo().alphaType());
    384         SkImageInfo decodeInfo = SkImageInfo::MakeN32(width, height, kUnpremul_SkAlphaType);
    385         if (SkCodec::kSuccess != codec->getPixels(decodeInfo, bitmap.getPixels(),
    386                                                   bitmap.rowBytes())) {
    387             SkDebugf("Could not decode input image.\n");
    388             return -1;
    389         }
    390         sk_sp<SkData> out = sk_tool_utils::EncodeImageToData(bitmap, SkEncodedImageFormat::kPNG,
    391                                                              100);
    392         if (!out) {
    393             SkDebugf("Failed to encode uncorrected image.\n");
    394             return -1;
    395         }
    396         SkFILEWStream bitmapStream(FLAGS_uncorrected[0]);
    397         if (!bitmapStream.write(out->data(), out->size())) {
    398             SkDebugf("Failed to write uncorrected image output.\n");
    399             return -1;
    400         }
    401         SkDebugf("%s\n", FLAGS_uncorrected[0]);
    402     }
    403 
    404     return 0;
    405 }
    406