1 // Copyright 2008-present Contributors to the OpenImageIO project.
2 // SPDX-License-Identifier: BSD-3-Clause
3 // https://github.com/OpenImageIO/oiio/blob/master/LICENSE.md
4 
5 
6 #include <iostream>
7 
8 #include "imageviewer.h"
9 #include <OpenImageIO/strutil.h>
10 
11 
IvImage(const std::string & filename,const ImageSpec * input_config)12 IvImage::IvImage(const std::string& filename, const ImageSpec* input_config)
13     : ImageBuf(filename, 0, 0, nullptr, input_config)
14     , m_thumbnail(NULL)
15     , m_thumbnail_valid(false)
16     , m_gamma(1)
17     , m_exposure(0)
18     , m_file_dataformat(TypeDesc::UNKNOWN)
19     , m_image_valid(false)
20     , m_auto_subimage(false)
21 {
22 }
23 
24 
25 
~IvImage()26 IvImage::~IvImage() { delete[] m_thumbnail; }
27 
28 
29 
30 bool
init_spec_iv(const std::string & filename,int subimage,int miplevel)31 IvImage::init_spec_iv(const std::string& filename, int subimage, int miplevel)
32 {
33     // invalidate info strings
34     m_shortinfo.clear();
35     m_longinfo.clear();
36 
37     // If we're changing mip levels or subimages, the pixels will no
38     // longer be valid.
39     if (subimage != this->subimage() || miplevel != this->miplevel())
40         m_image_valid = false;
41     bool ok = ImageBuf::init_spec(filename, subimage, miplevel);
42     if (ok && m_file_dataformat.basetype == TypeDesc::UNKNOWN) {
43         m_file_dataformat = spec().format;
44     }
45     string_view colorspace = spec().get_string_attribute("oiio:ColorSpace");
46     if (Strutil::istarts_with(colorspace, "GammaCorrected")) {
47         float g = Strutil::from_string<float>(colorspace.c_str() + 14);
48         if (g > 1.0 && g <= 3.0 /*sanity check*/) {
49             gamma(gamma() / g);
50         }
51     }
52     return ok;
53 }
54 
55 
56 
57 bool
read_iv(int subimage,int miplevel,bool force,TypeDesc format,ProgressCallback progress_callback,void * progress_callback_data,bool secondary_data)58 IvImage::read_iv(int subimage, int miplevel, bool force, TypeDesc format,
59                  ProgressCallback progress_callback,
60                  void* progress_callback_data, bool secondary_data)
61 {
62     // Don't read if we already have it in memory, unless force is true.
63     // FIXME: should we also check the time on the file to see if it's
64     // been updated since we last loaded?
65     if (m_image_valid && !force && subimage == this->subimage()
66         && miplevel != this->miplevel())
67         return true;
68 
69     m_image_valid = init_spec_iv(name(), subimage, miplevel);
70     if (m_image_valid)
71         m_image_valid = ImageBuf::read(subimage, miplevel, force, format,
72                                        progress_callback,
73                                        progress_callback_data);
74 
75     if (m_image_valid && secondary_data && spec().format == TypeDesc::UINT8) {
76         m_corrected_image.reset("", ImageSpec(spec().width, spec().height,
77                                               std::min(spec().nchannels, 4),
78                                               spec().format));
79     } else {
80         m_corrected_image.clear();
81     }
82     return m_image_valid;
83 }
84 
85 
86 
87 std::string
shortinfo() const88 IvImage::shortinfo() const
89 {
90     if (m_shortinfo.empty()) {
91         m_shortinfo = Strutil::sprintf("%d x %d", spec().width, spec().height);
92         if (spec().depth > 1)
93             m_shortinfo += Strutil::sprintf(" x %d", spec().depth);
94         m_shortinfo += Strutil::sprintf(" x %d channel %s (%.2f MB)",
95                                         spec().nchannels, m_file_dataformat,
96                                         (float)spec().image_bytes()
97                                             / (1024.0 * 1024.0));
98     }
99     return m_shortinfo;
100 }
101 
102 
103 
104 // Format name/value pairs as HTML table entries.
105 std::string
html_table_row(const char * name,const std::string & value)106 html_table_row(const char* name, const std::string& value)
107 {
108     std::string line = Strutil::sprintf("<tr><td><i>%s</i> : &nbsp;&nbsp;</td>",
109                                         name);
110     line += Strutil::sprintf("<td>%s</td></tr>\n", value.c_str());
111     return line;
112 }
113 
114 
115 std::string
html_table_row(const char * name,int value)116 html_table_row(const char* name, int value)
117 {
118     return html_table_row(name, Strutil::sprintf("%d", value));
119 }
120 
121 
122 std::string
html_table_row(const char * name,float value)123 html_table_row(const char* name, float value)
124 {
125     return html_table_row(name, Strutil::sprintf("%g", value));
126 }
127 
128 
129 
130 std::string
longinfo() const131 IvImage::longinfo() const
132 {
133     if (m_longinfo.empty()) {
134         const ImageSpec& m_spec(nativespec());
135         m_longinfo += "<table>";
136         //        m_longinfo += html_table_row (Strutil::sprintf("<b>%s</b>", m_name.c_str()).c_str(),
137         //                                std::string());
138         if (m_spec.depth <= 1)
139             m_longinfo += html_table_row("Dimensions",
140                                          Strutil::sprintf("%d x %d pixels",
141                                                           m_spec.width,
142                                                           m_spec.height));
143         else
144             m_longinfo += html_table_row("Dimensions",
145                                          Strutil::sprintf("%d x %d x %d pixels",
146                                                           m_spec.width,
147                                                           m_spec.height,
148                                                           m_spec.depth));
149         m_longinfo += html_table_row("Channels", m_spec.nchannels);
150         std::string chanlist;
151         for (int i = 0; i < m_spec.nchannels; ++i) {
152             chanlist += m_spec.channelnames[i].c_str();
153             if (i != m_spec.nchannels - 1)
154                 chanlist += ", ";
155         }
156         m_longinfo += html_table_row("Channel list", chanlist);
157         m_longinfo += html_table_row("File format", file_format_name());
158         m_longinfo += html_table_row("Data format", m_file_dataformat.c_str());
159         m_longinfo += html_table_row(
160             "Data size", Strutil::sprintf("%.2f MB", (float)m_spec.image_bytes()
161                                                          / (1024.0 * 1024.0)));
162         m_longinfo += html_table_row("Image origin",
163                                      Strutil::sprintf("%d, %d, %d", m_spec.x,
164                                                       m_spec.y, m_spec.z));
165         m_longinfo += html_table_row("Full/display size",
166                                      Strutil::sprintf("%d x %d x %d",
167                                                       m_spec.full_width,
168                                                       m_spec.full_height,
169                                                       m_spec.full_depth));
170         m_longinfo
171             += html_table_row("Full/display origin",
172                               Strutil::sprintf("%d, %d, %d", m_spec.full_x,
173                                                m_spec.full_y, m_spec.full_z));
174         if (m_spec.tile_width)
175             m_longinfo += html_table_row("Scanline/tile",
176                                          Strutil::sprintf("tiled %d x %d x %d",
177                                                           m_spec.tile_width,
178                                                           m_spec.tile_height,
179                                                           m_spec.tile_depth));
180         else
181             m_longinfo += html_table_row("Scanline/tile", "scanline");
182         if (m_spec.alpha_channel >= 0)
183             m_longinfo += html_table_row("Alpha channel", m_spec.alpha_channel);
184         if (m_spec.z_channel >= 0)
185             m_longinfo += html_table_row("Depth (z) channel", m_spec.z_channel);
186 
187         // Sort the metadata alphabetically, case-insensitive, but making
188         // sure that all non-namespaced attribs appear before namespaced
189         // attribs.
190         ParamValueList attribs = m_spec.extra_attribs;
191         attribs.sort(false /* sort case-insensitively */);
192         for (auto&& p : attribs) {
193             std::string s = m_spec.metadata_val(p, true);
194             m_longinfo += html_table_row(p.name().c_str(), s);
195         }
196 
197         m_longinfo += "</table>";
198     }
199     return m_longinfo;
200 }
201 
202 
203 
204 // Used by pixel_transform to convert from UINT8 to float.
205 static EightBitConverter<float> converter;
206 
207 
208 /// Helper routine: compute (gain*value)^invgamma
209 ///
210 
211 namespace {
212 
213 inline float
calc_exposure(float value,float gain,float invgamma)214 calc_exposure(float value, float gain, float invgamma)
215 {
216     if (invgamma != 1 && value >= 0)
217         return powf(gain * value, invgamma);
218     // Simple case - skip the expensive pow; also fall back to this
219     // case for negative values, for which gamma makes no sense.
220     return gain * value;
221 }
222 
223 }  // namespace
224 
225 
226 void
pixel_transform(bool srgb_to_linear,int color_mode,int select_channel)227 IvImage::pixel_transform(bool srgb_to_linear, int color_mode,
228                          int select_channel)
229 {
230     /// This table obeys the following function:
231     ///
232     ///   unsigned char srgb2linear(unsigned char x)
233     ///   {
234     ///       float x_f = x/255.0;
235     ///       float x_l = 0.0;
236     ///       if (x_f <= 0.04045)
237     ///           x_l = x_f/12.92;
238     ///       else
239     ///           x_l = powf((x_f+0.055)/1.055,2.4);
240     ///       return (unsigned char)(x_l * 255 + 0.5)
241     ///   }
242     ///
243     ///  It's used to transform from sRGB color space to linear color space.
244     // clang-format off
245     static const unsigned char srgb_to_linear_lut[256] = {
246         0, 0, 0, 0, 0, 0, 0, 1,
247         1, 1, 1, 1, 1, 1, 1, 1,
248         1, 1, 2, 2, 2, 2, 2, 2,
249         2, 2, 3, 3, 3, 3, 3, 3,
250         4, 4, 4, 4, 4, 5, 5, 5,
251         5, 6, 6, 6, 6, 7, 7, 7,
252         8, 8, 8, 8, 9, 9, 9, 10,
253         10, 10, 11, 11, 12, 12, 12, 13,
254         13, 13, 14, 14, 15, 15, 16, 16,
255         17, 17, 17, 18, 18, 19, 19, 20,
256         20, 21, 22, 22, 23, 23, 24, 24,
257         25, 25, 26, 27, 27, 28, 29, 29,
258         30, 30, 31, 32, 32, 33, 34, 35,
259         35, 36, 37, 37, 38, 39, 40, 41,
260         41, 42, 43, 44, 45, 45, 46, 47,
261         48, 49, 50, 51, 51, 52, 53, 54,
262         55, 56, 57, 58, 59, 60, 61, 62,
263         63, 64, 65, 66, 67, 68, 69, 70,
264         71, 72, 73, 74, 76, 77, 78, 79,
265         80, 81, 82, 84, 85, 86, 87, 88,
266         90, 91, 92, 93, 95, 96, 97, 99,
267         100, 101, 103, 104, 105, 107, 108, 109,
268         111, 112, 114, 115, 116, 118, 119, 121,
269         122, 124, 125, 127, 128, 130, 131, 133,
270         134, 136, 138, 139, 141, 142, 144, 146,
271         147, 149, 151, 152, 154, 156, 157, 159,
272         161, 163, 164, 166, 168, 170, 171, 173,
273         175, 177, 179, 181, 183, 184, 186, 188,
274         190, 192, 194, 196, 198, 200, 202, 204,
275         206, 208, 210, 212, 214, 216, 218, 220,
276         222, 224, 226, 229, 231, 233, 235, 237,
277         239, 242, 244, 246, 248, 250, 253, 255
278     };
279     // clang-format on
280     unsigned char correction_table[256];
281     int total_channels = spec().nchannels;
282     int color_channels = spec().nchannels;
283     int max_channels   = m_corrected_image.nchannels();
284 
285     // FIXME: Now with the iterator and data proxy in place, it should be
286     // trivial to apply the transformations to any kind of data, not just
287     // UINT8.
288     if (spec().format != TypeDesc::UINT8 || !m_corrected_image.localpixels()) {
289         return;
290     }
291 
292     if (color_channels > 3) {
293         color_channels = 3;
294     } else if (color_channels == 2) {
295         color_channels = 1;
296     }
297 
298     // This image is Luminance or Luminance + Alpha, and we are asked to show
299     // luminance.
300     if (color_channels == 1 && color_mode == 3) {
301         color_mode = 0;  // Just copy as usual.
302     }
303 
304     // Happy path:
305     if (!srgb_to_linear && color_mode <= 1 && m_gamma == 1.0
306         && m_exposure == 0.0) {
307         ImageBuf::ConstIterator<unsigned char, unsigned char> src(*this);
308         ImageBuf::Iterator<unsigned char, unsigned char> dst(m_corrected_image);
309         for (; src.valid(); ++src) {
310             dst.pos(src.x(), src.y());
311             for (int i = 0; i < max_channels; i++)
312                 dst[i] = src[i];
313         }
314         return;
315     }
316 
317     // fill the correction_table
318     if (gamma() == 1.0 && exposure() == 0.0) {
319         for (int pixelvalue = 0; pixelvalue < 256; ++pixelvalue) {
320             correction_table[pixelvalue] = pixelvalue;
321         }
322     } else {
323         float inv_gamma = 1.0 / gamma();
324         float gain      = powf(2.0f, exposure());
325         for (int pixelvalue = 0; pixelvalue < 256; ++pixelvalue) {
326             float pv_f = converter(pixelvalue);
327             pv_f = clamp(calc_exposure(pv_f, gain, inv_gamma), 0.0f, 1.0f);
328             correction_table[pixelvalue] = (unsigned char)(pv_f * 255 + 0.5);
329         }
330     }
331 
332     ImageBuf::ConstIterator<unsigned char, unsigned char> src(*this);
333     ImageBuf::Iterator<unsigned char, unsigned char> dst(m_corrected_image);
334     for (; src.valid(); ++src) {
335         dst.pos(src.x(), src.y());
336         if (color_mode == 0 || color_mode == 1) {
337             // RGBA, RGB modes.
338             int ch = 0;
339             for (ch = 0; ch < color_channels; ch++) {
340                 if (srgb_to_linear)
341                     dst[ch] = correction_table[srgb_to_linear_lut[src[ch]]];
342                 else
343                     dst[ch] = correction_table[src[ch]];
344             }
345             for (; ch < max_channels; ch++) {
346                 dst[ch] = src[ch];
347             }
348         } else if (color_mode == 3) {
349             // Convert RGB to luminance, (Rec. 709 luma coefficients).
350             float luminance;
351             if (srgb_to_linear) {
352                 luminance = converter(srgb_to_linear_lut[src[0]]) * 0.2126f
353                             + converter(srgb_to_linear_lut[src[1]]) * 0.7152f
354                             + converter(srgb_to_linear_lut[src[2]]) * 0.0722f;
355             } else {
356                 luminance = converter(src[0]) * 0.2126f
357                             + converter(src[1]) * 0.7152f
358                             + converter(src[2]) * 0.0722f;
359             }
360             unsigned char val
361                 = (unsigned char)(clamp(luminance, 0.0f, 1.0f) * 255.0 + 0.5);
362             val    = correction_table[val];
363             dst[0] = val;
364             dst[1] = val;
365             dst[2] = val;
366 
367             // Handle the rest of the channels
368             for (int ch = 3; ch < total_channels; ++ch) {
369                 dst[ch] = src[ch];
370             }
371         } else {  // Single channel, heatmap.
372             unsigned char v = 0;
373             if (select_channel < color_channels) {
374                 if (srgb_to_linear)
375                     v = correction_table[srgb_to_linear_lut[src[select_channel]]];
376                 else
377                     v = correction_table[src[select_channel]];
378             } else if (select_channel < total_channels) {
379                 v = src[select_channel];
380             }
381             int ch = 0;
382             for (; ch < color_channels; ++ch) {
383                 dst[ch] = v;
384             }
385             for (; ch < max_channels; ++ch) {
386                 dst[ch] = src[ch];
387             }
388         }
389     }
390 }
391 
392 
393 
394 void
invalidate()395 IvImage::invalidate()
396 {
397     ustring filename(name());
398     reset(filename.string());
399     m_thumbnail_valid = false;
400     m_image_valid     = false;
401     if (imagecache())
402         imagecache()->invalidate(filename);
403 }
404