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
23DEFINE_string(input, "input.png", "A path to the input image or icc profile.");
24DEFINE_string(gamut_output, "gamut_output.png", "A path to the output gamut image.");
25DEFINE_string(gamma_output, "gamma_output.png", "A path to the output gamma image.");
26DEFINE_bool(sRGB_gamut, false, "Draws the sRGB gamut on the gamut visualization.");
27DEFINE_bool(adobeRGB, false, "Draws the Adobe RGB gamut on the gamut visualization.");
28DEFINE_bool(sRGB_gamma, false, "Draws the sRGB gamma on all gamma output images.");
29DEFINE_string(uncorrected, "", "A path to reencode the uncorrected input image.");
30
31static const char* kRGBChannelNames[3] = {
32    "Red  ", "Green", "Blue "
33};
34
35static const SkColor kRGBChannelColors[3] = {
36    SkColorSetARGB(164, 255, 32, 32),
37    SkColorSetARGB(164, 32, 255, 32),
38    SkColorSetARGB(164, 32, 32, 255)
39};
40
41static 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
58static 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
92static 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
97static 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 */
189static 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 */
207static 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
214static 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
261int 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