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 #include <cmath>
6 #include <iostream>
7 #ifndef _WIN32
8 #    include <unistd.h>
9 #endif
10 #include <vector>
11 
12 #include "imageviewer.h"
13 #include "ivgl.h"
14 
15 #include <QApplication>
16 #include <QComboBox>
17 #include <QDesktopWidget>
18 #include <QFileDialog>
19 #include <QKeyEvent>
20 #include <QLabel>
21 #include <QMenu>
22 #include <QMenuBar>
23 #include <QMessageBox>
24 #include <QProgressBar>
25 #include <QResizeEvent>
26 #include <QSettings>
27 #include <QSpinBox>
28 #include <QStatusBar>
29 #include <QTimer>
30 
31 #include <OpenImageIO/dassert.h>
32 #include <OpenImageIO/filesystem.h>
33 #include <OpenImageIO/strutil.h>
34 #include <OpenImageIO/sysutil.h>
35 #include <OpenImageIO/timer.h>
36 
37 #include "ivutils.h"
38 
39 
40 namespace {
41 
42 inline bool
IsSpecSrgb(const ImageSpec & spec)43 IsSpecSrgb(const ImageSpec& spec)
44 {
45     return Strutil::iequals(spec.get_string_attribute("oiio:ColorSpace"),
46                             "sRGB");
47 }
48 
49 }  // namespace
50 
51 
52 // clang-format off
53 static const char *s_file_filters = ""
54     "Image Files (*.bmp *.cin *.dcm *.dds *.dpx *.f3d *.fits *.gif *.hdr *.ico *.iff "
55     "*.jpg *.jpe *.jpeg *.jif *.jfif *.jfi *.jp2 *.j2k *.exr *.png *.pbm *.pgm "
56     "*.ppm *.psd *.ptex *.rla *.sgi *.rgb *.rgba *.bw *.int *.inta *.pic *.tga "
57     "*.tpic *.tif *.tiff *.tx *.env *.sm *.vsm *.webp *.zfile);;"
58     "BMP (*.bmp);;"
59     "Cineon (*.cin);;"
60     "Direct Draw Surface (*.dds);;"
61     "DICOM (*.dcm);;"
62     "DPX (*.dpx);;"
63     "Field3D (*.f3d);;"
64     "FITS (*.fits);;"
65     "GIF (*.gif);;"
66     "HDR/RGBE (*.hdr);;"
67     "Icon (*.ico);;"
68     "IFF (*.iff);;"
69     "JPEG (*.jpg *.jpe *.jpeg *.jif *.jfif *.jfi);;"
70     "JPEG-2000 (*.jp2 *.j2k);;"
71     "OpenEXR (*.exr);;"
72     "PhotoShop (*.psd);;"
73     "Portable Network Graphics (*.png);;"
74     "PNM / Netpbm (*.pbm *.pgm *.ppm);;"
75     "Ptex (*.ptex);;"
76     "RLA (*.rla);;"
77     "SGI (*.sgi *.rgb *.rgba *.bw *.int *.inta);;"
78     "Softimage PIC (*.pic);;"
79     "Targa (*.tga *.tpic);;"
80     "TIFF (*.tif *.tiff *.tx *.env *.sm *.vsm);;"
81     "Webp (*.webp);;"
82     "Zfile (*.zfile);;"
83     "All Files (*)";
84 // clang-format on
85 
86 
87 
ImageViewer()88 ImageViewer::ImageViewer()
89     : infoWindow(NULL)
90     , preferenceWindow(NULL)
91     , darkPaletteBox(NULL)
92     , m_current_image(-1)
93     , m_current_channel(0)
94     , m_color_mode(RGBA)
95     , m_last_image(-1)
96     , m_zoom(1)
97     , m_fullscreen(false)
98     , m_default_gamma(1)
99     , m_darkPalette(false)
100 {
101     readSettings(false);
102 
103     float gam = Strutil::stof(Sysutil::getenv("GAMMA"));
104     if (gam >= 0.1 && gam <= 5)
105         m_default_gamma = gam;
106     // FIXME -- would be nice to have a more nuanced approach to display
107     // color space, in particular knowing whether the display is sRGB.
108     // Also, some time in the future we may want a real 3D LUT for
109     // "film look", etc.
110 
111     if (darkPalette())
112         m_palette = QPalette(Qt::darkGray);  // darkGray?
113     else
114         m_palette = QPalette();
115     QApplication::setPalette(m_palette);  // FIXME -- why not work?
116     this->setPalette(m_palette);
117 
118     slideTimer       = new QTimer();
119     slideDuration_ms = 5000;
120     slide_loop       = true;
121     glwin            = new IvGL(this, *this);
122     glwin->setPalette(m_palette);
123     glwin->resize(m_default_width, m_default_height);
124     setCentralWidget(glwin);
125 
126     createActions();
127     createMenus();
128     createToolBars();
129     createStatusBar();
130 
131     readSettings();
132 
133     setWindowTitle(tr("Image Viewer"));
134     resize(m_default_width, m_default_height);
135     //    setSizePolicy (QSizePolicy::Ignored, QSizePolicy::Ignored);
136 }
137 
138 
139 
~ImageViewer()140 ImageViewer::~ImageViewer()
141 {
142     for (auto i : m_images)
143         delete i;
144 }
145 
146 
147 
148 void
closeEvent(QCloseEvent *)149 ImageViewer::closeEvent(QCloseEvent*)
150 {
151     writeSettings();
152 }
153 
154 
155 
156 void
createActions()157 ImageViewer::createActions()
158 {
159     openAct = new QAction(tr("&Open..."), this);
160     openAct->setShortcut(tr("Ctrl+O"));
161     connect(openAct, SIGNAL(triggered()), this, SLOT(open()));
162 
163     for (auto& i : openRecentAct) {
164         i = new QAction(this);
165         i->setVisible(false);
166         connect(i, SIGNAL(triggered()), this, SLOT(openRecentFile()));
167     }
168 
169     reloadAct = new QAction(tr("&Reload image"), this);
170     reloadAct->setShortcut(tr("Ctrl+R"));
171     connect(reloadAct, SIGNAL(triggered()), this, SLOT(reload()));
172 
173     closeImgAct = new QAction(tr("&Close Image"), this);
174     closeImgAct->setShortcut(tr("Ctrl+W"));
175     connect(closeImgAct, SIGNAL(triggered()), this, SLOT(closeImg()));
176 
177     saveAsAct = new QAction(tr("&Save As..."), this);
178     saveAsAct->setShortcut(tr("Ctrl+S"));
179     connect(saveAsAct, SIGNAL(triggered()), this, SLOT(saveAs()));
180 
181     saveWindowAsAct = new QAction(tr("Save Window As..."), this);
182     connect(saveWindowAsAct, SIGNAL(triggered()), this, SLOT(saveWindowAs()));
183 
184     saveSelectionAsAct = new QAction(tr("Save Selection As..."), this);
185     connect(saveSelectionAsAct, SIGNAL(triggered()), this,
186             SLOT(saveSelectionAs()));
187 
188     printAct = new QAction(tr("&Print..."), this);
189     printAct->setShortcut(tr("Ctrl+P"));
190     printAct->setEnabled(false);
191     connect(printAct, SIGNAL(triggered()), this, SLOT(print()));
192 
193     deleteCurrentImageAct = new QAction(tr("&Delete from disk"), this);
194     deleteCurrentImageAct->setShortcut(tr("Delete"));
195     connect(deleteCurrentImageAct, SIGNAL(triggered()), this,
196             SLOT(deleteCurrentImage()));
197 
198     editPreferencesAct = new QAction(tr("&Preferences..."), this);
199     editPreferencesAct->setShortcut(tr("Ctrl+,"));
200     editPreferencesAct->setEnabled(true);
201     connect(editPreferencesAct, SIGNAL(triggered()), this,
202             SLOT(editPreferences()));
203 
204     exitAct = new QAction(tr("E&xit"), this);
205     exitAct->setShortcut(tr("Ctrl+Q"));
206     connect(exitAct, SIGNAL(triggered()), this, SLOT(close()));
207 
208     exposurePlusOneTenthStopAct = new QAction(tr("Exposure +1/10 stop"), this);
209     exposurePlusOneTenthStopAct->setShortcut(tr("]"));
210     connect(exposurePlusOneTenthStopAct, SIGNAL(triggered()), this,
211             SLOT(exposurePlusOneTenthStop()));
212 
213     exposurePlusOneHalfStopAct = new QAction(tr("Exposure +1/2 stop"), this);
214     exposurePlusOneHalfStopAct->setShortcut(tr("}"));
215     connect(exposurePlusOneHalfStopAct, SIGNAL(triggered()), this,
216             SLOT(exposurePlusOneHalfStop()));
217 
218     exposureMinusOneTenthStopAct = new QAction(tr("Exposure -1/10 stop"), this);
219     exposureMinusOneTenthStopAct->setShortcut(tr("["));
220     connect(exposureMinusOneTenthStopAct, SIGNAL(triggered()), this,
221             SLOT(exposureMinusOneTenthStop()));
222 
223     exposureMinusOneHalfStopAct = new QAction(tr("Exposure -1/2 stop"), this);
224     exposureMinusOneHalfStopAct->setShortcut(tr("{"));
225     connect(exposureMinusOneHalfStopAct, SIGNAL(triggered()), this,
226             SLOT(exposureMinusOneHalfStop()));
227 
228     gammaPlusAct = new QAction(tr("Gamma +0.1"), this);
229     gammaPlusAct->setShortcut(tr(")"));
230     connect(gammaPlusAct, SIGNAL(triggered()), this, SLOT(gammaPlus()));
231 
232     gammaMinusAct = new QAction(tr("Gamma -0.1"), this);
233     gammaMinusAct->setShortcut(tr("("));
234     connect(gammaMinusAct, SIGNAL(triggered()), this, SLOT(gammaMinus()));
235 
236     viewChannelFullAct = new QAction(tr("Full Color"), this);
237     viewChannelFullAct->setShortcut(tr("c"));
238     viewChannelFullAct->setCheckable(true);
239     viewChannelFullAct->setChecked(true);
240     connect(viewChannelFullAct, SIGNAL(triggered()), this,
241             SLOT(viewChannelFull()));
242 
243     viewChannelRedAct = new QAction(tr("Red"), this);
244     viewChannelRedAct->setShortcut(tr("r"));
245     viewChannelRedAct->setCheckable(true);
246     connect(viewChannelRedAct, SIGNAL(triggered()), this,
247             SLOT(viewChannelRed()));
248 
249     viewChannelGreenAct = new QAction(tr("Green"), this);
250     viewChannelGreenAct->setShortcut(tr("g"));
251     viewChannelGreenAct->setCheckable(true);
252     connect(viewChannelGreenAct, SIGNAL(triggered()), this,
253             SLOT(viewChannelGreen()));
254 
255     viewChannelBlueAct = new QAction(tr("Blue"), this);
256     viewChannelBlueAct->setShortcut(tr("b"));
257     viewChannelBlueAct->setCheckable(true);
258     connect(viewChannelBlueAct, SIGNAL(triggered()), this,
259             SLOT(viewChannelBlue()));
260 
261     viewChannelAlphaAct = new QAction(tr("Alpha"), this);
262     viewChannelAlphaAct->setShortcut(tr("a"));
263     viewChannelAlphaAct->setCheckable(true);
264     connect(viewChannelAlphaAct, SIGNAL(triggered()), this,
265             SLOT(viewChannelAlpha()));
266 
267     viewColorLumAct = new QAction(tr("Luminance"), this);
268     viewColorLumAct->setShortcut(tr("l"));
269     viewColorLumAct->setCheckable(true);
270     connect(viewColorLumAct, SIGNAL(triggered()), this,
271             SLOT(viewChannelLuminance()));
272 
273     viewColorRGBAAct = new QAction(tr("RGBA"), this);
274     //viewColorRGBAAct->setShortcut (tr("Ctrl+c"));
275     viewColorRGBAAct->setCheckable(true);
276     viewColorRGBAAct->setChecked(true);
277     connect(viewColorRGBAAct, SIGNAL(triggered()), this, SLOT(viewColorRGBA()));
278 
279     viewColorRGBAct = new QAction(tr("RGB"), this);
280     //viewColorRGBAct->setShortcut (tr("Ctrl+a"));
281     viewColorRGBAct->setCheckable(true);
282     connect(viewColorRGBAct, SIGNAL(triggered()), this, SLOT(viewColorRGB()));
283 
284     viewColor1ChAct = new QAction(tr("Single channel"), this);
285     viewColor1ChAct->setShortcut(tr("1"));
286     viewColor1ChAct->setCheckable(true);
287     connect(viewColor1ChAct, SIGNAL(triggered()), this, SLOT(viewColor1Ch()));
288 
289     viewColorHeatmapAct = new QAction(tr("Single channel (Heatmap)"), this);
290     viewColorHeatmapAct->setShortcut(tr("h"));
291     viewColorHeatmapAct->setCheckable(true);
292     connect(viewColorHeatmapAct, SIGNAL(triggered()), this,
293             SLOT(viewColorHeatmap()));
294 
295     viewChannelPrevAct = new QAction(tr("Prev Channel"), this);
296     viewChannelPrevAct->setShortcut(tr(","));
297     connect(viewChannelPrevAct, SIGNAL(triggered()), this,
298             SLOT(viewChannelPrev()));
299 
300     viewChannelNextAct = new QAction(tr("Next Channel"), this);
301     viewChannelNextAct->setShortcut(tr("."));
302     connect(viewChannelNextAct, SIGNAL(triggered()), this,
303             SLOT(viewChannelNext()));
304 
305     viewSubimagePrevAct = new QAction(tr("Prev Subimage"), this);
306     viewSubimagePrevAct->setShortcut(tr("<"));
307     connect(viewSubimagePrevAct, SIGNAL(triggered()), this,
308             SLOT(viewSubimagePrev()));
309 
310     viewSubimageNextAct = new QAction(tr("Next Subimage"), this);
311     viewSubimageNextAct->setShortcut(tr(">"));
312     connect(viewSubimageNextAct, SIGNAL(triggered()), this,
313             SLOT(viewSubimageNext()));
314 
315     zoomInAct = new QAction(tr("Zoom &In"), this);
316     zoomInAct->setShortcut(tr("Ctrl++"));
317     connect(zoomInAct, SIGNAL(triggered()), this, SLOT(zoomIn()));
318 
319     zoomOutAct = new QAction(tr("Zoom &Out"), this);
320     zoomOutAct->setShortcut(tr("Ctrl+-"));
321     connect(zoomOutAct, SIGNAL(triggered()), this, SLOT(zoomOut()));
322 
323     normalSizeAct = new QAction(tr("&Normal Size (1:1)"), this);
324     normalSizeAct->setShortcut(tr("Ctrl+0"));
325     connect(normalSizeAct, SIGNAL(triggered()), this, SLOT(normalSize()));
326 
327     fitWindowToImageAct = new QAction(tr("&Fit Window to Image"), this);
328     fitWindowToImageAct->setShortcut(tr("f"));
329     connect(fitWindowToImageAct, SIGNAL(triggered()), this,
330             SLOT(fitWindowToImage()));
331 
332     fitImageToWindowAct = new QAction(tr("Fit Image to Window"), this);
333     //    fitImageToWindowAct->setEnabled(false);
334     fitImageToWindowAct->setCheckable(true);
335     fitImageToWindowAct->setShortcut(tr("Alt+f"));
336     connect(fitImageToWindowAct, SIGNAL(triggered()), this,
337             SLOT(fitImageToWindow()));
338 
339     fullScreenAct = new QAction(tr("Full screen"), this);
340     //    fullScreenAct->setEnabled(false);
341     //    fullScreenAct->setCheckable(true);
342     fullScreenAct->setShortcut(tr("Ctrl+f"));
343     connect(fullScreenAct, SIGNAL(triggered()), this, SLOT(fullScreenToggle()));
344 
345     aboutAct = new QAction(tr("&About"), this);
346     connect(aboutAct, SIGNAL(triggered()), this, SLOT(about()));
347 
348     prevImageAct = new QAction(tr("Previous Image"), this);
349     prevImageAct->setShortcut(tr("PgUp"));
350     //    prevImageAct->setEnabled(true);
351     connect(prevImageAct, SIGNAL(triggered()), this, SLOT(prevImage()));
352 
353     nextImageAct = new QAction(tr("Next Image"), this);
354     nextImageAct->setShortcut(tr("PgDown"));
355     //    nextImageAct->setEnabled(true);
356     connect(nextImageAct, SIGNAL(triggered()), this, SLOT(nextImage()));
357 
358     toggleImageAct = new QAction(tr("Toggle image"), this);
359     toggleImageAct->setShortcut(tr("T"));
360     //    toggleImageAct->setEnabled(true);
361     connect(toggleImageAct, SIGNAL(triggered()), this, SLOT(toggleImage()));
362 
363     slideShowAct = new QAction(tr("Start Slide Show"), this);
364     connect(slideShowAct, SIGNAL(triggered()), this, SLOT(slideShow()));
365 
366     slideLoopAct = new QAction(tr("Loop slide show"), this);
367     slideLoopAct->setCheckable(true);
368     slideLoopAct->setChecked(true);
369     connect(slideLoopAct, SIGNAL(triggered()), this, SLOT(slideLoop()));
370 
371     slideNoLoopAct = new QAction(tr("Stop at end"), this);
372     slideNoLoopAct->setCheckable(true);
373     connect(slideNoLoopAct, SIGNAL(triggered()), this, SLOT(slideNoLoop()));
374 
375     sortByNameAct = new QAction(tr("By Name"), this);
376     connect(sortByNameAct, SIGNAL(triggered()), this, SLOT(sortByName()));
377 
378     sortByPathAct = new QAction(tr("By File Path"), this);
379     connect(sortByPathAct, SIGNAL(triggered()), this, SLOT(sortByPath()));
380 
381     sortByImageDateAct = new QAction(tr("By Image Date"), this);
382     connect(sortByImageDateAct, SIGNAL(triggered()), this,
383             SLOT(sortByImageDate()));
384 
385     sortByFileDateAct = new QAction(tr("By File Date"), this);
386     connect(sortByFileDateAct, SIGNAL(triggered()), this,
387             SLOT(sortByFileDate()));
388 
389     sortReverseAct = new QAction(tr("Reverse current order"), this);
390     connect(sortReverseAct, SIGNAL(triggered()), this, SLOT(sortReverse()));
391 
392     showInfoWindowAct = new QAction(tr("&Image info..."), this);
393     showInfoWindowAct->setShortcut(tr("Ctrl+I"));
394     //    showInfoWindowAct->setEnabled(true);
395     connect(showInfoWindowAct, SIGNAL(triggered()), this,
396             SLOT(showInfoWindow()));
397 
398     showPixelviewWindowAct = new QAction(tr("&Pixel closeup view..."), this);
399     showPixelviewWindowAct->setCheckable(true);
400     showPixelviewWindowAct->setShortcut(tr("P"));
401     connect(showPixelviewWindowAct, SIGNAL(triggered()), this,
402             SLOT(showPixelviewWindow()));
403 
404     pixelviewFollowsMouseBox = new QCheckBox(tr("Pixel view follows mouse"));
405     pixelviewFollowsMouseBox->setChecked(false);
406     linearInterpolationBox = new QCheckBox(tr("Linear interpolation"));
407     linearInterpolationBox->setChecked(true);
408     darkPaletteBox = new QCheckBox(tr("Dark palette"));
409     darkPaletteBox->setChecked(true);
410     autoMipmap = new QCheckBox(tr("Generate mipmaps (requires restart)"));
411     autoMipmap->setChecked(false);
412 
413     maxMemoryICLabel = new QLabel(
414         tr("Image Cache max memory (requires restart)"));
415     maxMemoryIC = new QSpinBox();
416     if (sizeof(void*) == 4)
417         maxMemoryIC->setRange(128, 2048);  //2GB is enough for 32 bit machines
418     else
419         maxMemoryIC->setRange(128, 8192);  //8GB probably ok for 64 bit
420     maxMemoryIC->setSingleStep(64);
421     maxMemoryIC->setSuffix(" MB");
422 
423     slideShowDurationLabel = new QLabel(tr("Slide Show delay"));
424     slideShowDuration      = new QSpinBox();
425     slideShowDuration->setRange(1, 3600);
426     slideShowDuration->setSingleStep(1);
427     slideShowDuration->setSuffix(" s");
428     slideShowDuration->setAccelerated(true);
429     connect(slideShowDuration, SIGNAL(valueChanged(int)), this,
430             SLOT(setSlideShowDuration(int)));
431 }
432 
433 
434 
435 void
createMenus()436 ImageViewer::createMenus()
437 {
438     openRecentMenu = new QMenu(tr("Open recent..."), this);
439     for (auto& i : openRecentAct)
440         openRecentMenu->addAction(i);
441 
442     fileMenu = new QMenu(tr("&File"), this);
443     fileMenu->addAction(openAct);
444     fileMenu->addMenu(openRecentMenu);
445     fileMenu->addAction(reloadAct);
446     fileMenu->addAction(closeImgAct);
447     fileMenu->addSeparator();
448     fileMenu->addAction(saveAsAct);
449     fileMenu->addAction(saveWindowAsAct);
450     fileMenu->addAction(saveSelectionAsAct);
451     fileMenu->addSeparator();
452     fileMenu->addAction(printAct);
453     fileMenu->addAction(deleteCurrentImageAct);
454     fileMenu->addSeparator();
455     fileMenu->addAction(editPreferencesAct);
456     fileMenu->addAction(exitAct);
457     menuBar()->addMenu(fileMenu);
458 
459     // Copy
460     // Paste
461     // Clear selection
462     // radio: prioritize selection, crop selection
463 
464     expgamMenu = new QMenu(tr("Exposure/gamma"));  // submenu
465     expgamMenu->addAction(exposureMinusOneHalfStopAct);
466     expgamMenu->addAction(exposureMinusOneTenthStopAct);
467     expgamMenu->addAction(exposurePlusOneHalfStopAct);
468     expgamMenu->addAction(exposurePlusOneTenthStopAct);
469     expgamMenu->addAction(gammaMinusAct);
470     expgamMenu->addAction(gammaPlusAct);
471 
472     //    imageMenu = new QMenu(tr("&Image"), this);
473     //    menuBar()->addMenu (imageMenu);
474     slideMenu = new QMenu(tr("Slide Show"));
475     slideMenu->addAction(slideShowAct);
476     slideMenu->addAction(slideLoopAct);
477     slideMenu->addAction(slideNoLoopAct);
478 
479     sortMenu = new QMenu(tr("Sort"));
480     sortMenu->addAction(sortByNameAct);
481     sortMenu->addAction(sortByPathAct);
482     sortMenu->addAction(sortByImageDateAct);
483     sortMenu->addAction(sortByFileDateAct);
484     sortMenu->addAction(sortReverseAct);
485 
486     channelMenu = new QMenu(tr("Channels"));
487     // Color mode: true, random, falsegrgbacCrgR
488     channelMenu->addAction(viewChannelFullAct);
489     channelMenu->addAction(viewChannelRedAct);
490     channelMenu->addAction(viewChannelGreenAct);
491     channelMenu->addAction(viewChannelBlueAct);
492     channelMenu->addAction(viewChannelAlphaAct);
493     channelMenu->addAction(viewChannelPrevAct);
494     channelMenu->addAction(viewChannelNextAct);
495 
496     colormodeMenu = new QMenu(tr("Color mode"));
497     colormodeMenu->addAction(viewColorRGBAAct);
498     colormodeMenu->addAction(viewColorRGBAct);
499     colormodeMenu->addAction(viewColor1ChAct);
500     colormodeMenu->addAction(viewColorLumAct);
501     colormodeMenu->addAction(viewColorHeatmapAct);
502 
503     viewMenu = new QMenu(tr("&View"), this);
504     viewMenu->addAction(prevImageAct);
505     viewMenu->addAction(nextImageAct);
506     viewMenu->addAction(toggleImageAct);
507     viewMenu->addSeparator();
508     viewMenu->addAction(zoomInAct);
509     viewMenu->addAction(zoomOutAct);
510     viewMenu->addAction(normalSizeAct);
511     viewMenu->addAction(fitWindowToImageAct);
512     viewMenu->addAction(fitImageToWindowAct);
513     viewMenu->addAction(fullScreenAct);
514     viewMenu->addSeparator();
515     viewMenu->addAction(viewSubimagePrevAct);
516     viewMenu->addAction(viewSubimageNextAct);
517     viewMenu->addMenu(channelMenu);
518     viewMenu->addMenu(colormodeMenu);
519     viewMenu->addMenu(expgamMenu);
520     menuBar()->addMenu(viewMenu);
521     // Full screen mode
522     // prev subimage <, next subimage >
523     // fg/bg image...
524 
525     toolsMenu = new QMenu(tr("&Tools"), this);
526     // Mode: select, zoom, pan, wipe
527     toolsMenu->addAction(showInfoWindowAct);
528     toolsMenu->addAction(showPixelviewWindowAct);
529     toolsMenu->addMenu(slideMenu);
530     toolsMenu->addMenu(sortMenu);
531 
532     // Menus, toolbars, & status
533     // Annotate
534     // [check] overwrite render
535     // connect renderer
536     // kill renderer
537     // store render
538     // Playback: forward, reverse, faster, slower, loop/pingpong
539     menuBar()->addMenu(toolsMenu);
540 
541     helpMenu = new QMenu(tr("&Help"), this);
542     helpMenu->addAction(aboutAct);
543     menuBar()->addMenu(helpMenu);
544     // Bring up user's guide
545 }
546 
547 
548 
549 void
createToolBars()550 ImageViewer::createToolBars()
551 {
552 #if 0
553     fileToolBar = addToolBar(tr("File"));
554     fileToolBar->addAction(newAct);
555     fileToolBar->addAction(openAct);
556     fileToolBar->addAction(saveAct);
557 
558     editToolBar = addToolBar(tr("Edit"));
559     editToolBar->addAction(cutAct);
560     editToolBar->addAction(copyAct);
561     editToolBar->addAction(pasteAct);
562 #endif
563 }
564 
565 
566 
567 void
createStatusBar()568 ImageViewer::createStatusBar()
569 {
570     statusImgInfo = new QLabel;
571     statusBar()->addWidget(statusImgInfo);
572 
573     statusViewInfo = new QLabel;
574     statusBar()->addWidget(statusViewInfo);
575 
576     statusProgress = new QProgressBar;
577     statusProgress->setRange(0, 100);
578     statusProgress->reset();
579     statusBar()->addWidget(statusProgress);
580 
581     mouseModeComboBox = new QComboBox;
582     mouseModeComboBox->addItem(tr("Zoom"));
583     mouseModeComboBox->addItem(tr("Pan"));
584     mouseModeComboBox->addItem(tr("Wipe"));
585     mouseModeComboBox->addItem(tr("Select"));
586     mouseModeComboBox->addItem(tr("Annotate"));
587     // Note: the order of the above MUST match the order of enum MouseMode
588     statusBar()->addWidget(mouseModeComboBox);
589     mouseModeComboBox->hide();
590 }
591 
592 
593 
594 void
readSettings(bool ui_is_set_up)595 ImageViewer::readSettings(bool ui_is_set_up)
596 {
597     QSettings settings("OpenImageIO", "iv");
598     m_darkPalette = settings.value("darkPalette").toBool();
599     if (!ui_is_set_up)
600         return;
601     pixelviewFollowsMouseBox->setChecked(
602         settings.value("pixelviewFollowsMouse").toBool());
603     linearInterpolationBox->setChecked(
604         settings.value("linearInterpolation").toBool());
605     darkPaletteBox->setChecked(settings.value("darkPalette").toBool());
606     QStringList recent = settings.value("RecentFiles").toStringList();
607     for (auto&& s : recent)
608         addRecentFile(s.toStdString());
609     updateRecentFilesMenu();  // only safe because it's called after menu setup
610 
611     autoMipmap->setChecked(settings.value("autoMipmap", false).toBool());
612     if (sizeof(void*) == 4)  // 32 bit or 64?
613         maxMemoryIC->setValue(settings.value("maxMemoryIC", 512).toInt());
614     else
615         maxMemoryIC->setValue(settings.value("maxMemoryIC", 2048).toInt());
616     slideShowDuration->setValue(
617         settings.value("slideShowDuration", 10).toInt());
618 
619     ImageCache* imagecache = ImageCache::create(true);
620     imagecache->attribute("automip", autoMipmap->isChecked());
621     imagecache->attribute("max_memory_MB", (float)maxMemoryIC->value());
622 }
623 
624 
625 
626 void
writeSettings()627 ImageViewer::writeSettings()
628 {
629     QSettings settings("OpenImageIO", "iv");
630     settings.setValue("pixelviewFollowsMouse",
631                       pixelviewFollowsMouseBox->isChecked());
632     settings.setValue("linearInterpolation",
633                       linearInterpolationBox->isChecked());
634     settings.setValue("darkPalette", darkPaletteBox->isChecked());
635     settings.setValue("autoMipmap", autoMipmap->isChecked());
636     settings.setValue("maxMemoryIC", maxMemoryIC->value());
637     settings.setValue("slideShowDuration", slideShowDuration->value());
638     QStringList recent;
639     for (auto&& s : m_recent_files)
640         recent.push_front(QString(s.c_str()));
641     settings.setValue("RecentFiles", recent);
642 }
643 
644 
645 
646 bool
image_progress_callback(void * opaque,float done)647 image_progress_callback(void* opaque, float done)
648 {
649     ImageViewer* viewer = (ImageViewer*)opaque;
650     viewer->statusProgress->setValue((int)(done * 100));
651     QApplication::processEvents();
652     return false;
653 }
654 
655 
656 
657 void
open()658 ImageViewer::open()
659 {
660     static QString openPath = QDir::currentPath();
661     QFileDialog dialog(NULL, tr("Open File(s)"), openPath, tr(s_file_filters));
662     dialog.setAcceptMode(QFileDialog::AcceptOpen);
663     dialog.setFileMode(QFileDialog::ExistingFiles);
664     if (!dialog.exec())
665         return;
666     openPath          = dialog.directory().path();
667     QStringList names = dialog.selectedFiles();
668 
669     int old_lastimage = m_images.size() - 1;
670     for (auto& name : names) {
671         std::string filename = name.toUtf8().data();
672         if (filename.empty())
673             continue;
674         add_image(filename);
675         //        int n = m_images.size()-1;
676         //        IvImage *newimage = m_images[n];
677         //        newimage->read_iv (0, false, image_progress_callback, this);
678     }
679     if (old_lastimage >= 0) {
680         // Otherwise, add_image already did this for us.
681         current_image(old_lastimage + 1);
682         fitWindowToImage(true, true);
683     }
684 }
685 
686 
687 
688 void
openRecentFile()689 ImageViewer::openRecentFile()
690 {
691     QAction* action = qobject_cast<QAction*>(sender());
692     if (action) {
693         std::string filename = action->data().toString().toStdString();
694         // If it's an image we already have loaded, just switch to it
695         // (and reload) rather than loading a second copy.
696         for (size_t i = 0; i < m_images.size(); ++i) {
697             if (m_images[i]->name() == filename) {
698                 current_image(i);
699                 reload();
700                 return;
701             }
702         }
703         // It's not an image we already have loaded
704         add_image(filename);
705         if (m_images.size() > 1) {
706             // Otherwise, add_image already did this for us.
707             current_image(m_images.size() - 1);
708             fitWindowToImage(true, true);
709         }
710     }
711 }
712 
713 
714 
715 void
addRecentFile(const std::string & name)716 ImageViewer::addRecentFile(const std::string& name)
717 {
718     removeRecentFile(name);
719     m_recent_files.insert(m_recent_files.begin(), name);
720     if (m_recent_files.size() > MaxRecentFiles)
721         m_recent_files.resize(MaxRecentFiles);
722 }
723 
724 
725 
726 void
removeRecentFile(const std::string & name)727 ImageViewer::removeRecentFile(const std::string& name)
728 {
729     for (size_t i = 0; i < m_recent_files.size(); ++i) {
730         if (m_recent_files[i] == name) {
731             m_recent_files.erase(m_recent_files.begin() + i);
732             --i;
733         }
734     }
735 }
736 
737 
738 
739 void
updateRecentFilesMenu()740 ImageViewer::updateRecentFilesMenu()
741 {
742     for (size_t i = 0; i < MaxRecentFiles; ++i) {
743         if (i < m_recent_files.size()) {
744             std::string name = Filesystem::filename(m_recent_files[i]);
745             openRecentAct[i]->setText(QString::fromStdString(name));
746             openRecentAct[i]->setData(m_recent_files[i].c_str());
747             openRecentAct[i]->setVisible(true);
748         } else {
749             openRecentAct[i]->setVisible(false);
750         }
751     }
752 }
753 
754 
755 
756 void
reload()757 ImageViewer::reload()
758 {
759     if (m_images.empty())
760         return;
761     IvImage* newimage = m_images[m_current_image];
762     newimage->invalidate();
763     //glwin->trigger_redraw ();
764     displayCurrentImage();
765 }
766 
767 
768 
769 void
add_image(const std::string & filename)770 ImageViewer::add_image(const std::string& filename)
771 {
772     if (filename.empty())
773         return;
774     ImageSpec config;
775     if (rawcolor())
776         config.attribute("oiio:RawColor", 1);
777     IvImage* newimage = new IvImage(filename, &config);
778     newimage->gamma(m_default_gamma);
779     m_images.push_back(newimage);
780     addRecentFile(filename);
781     updateRecentFilesMenu();
782 
783 #if 0
784     if (getspec) {
785         if (! newimage->init_spec (filename)) {
786             QMessageBox::information (this, tr("iv Image Viewer"),
787                               tr("%1").arg(newimage->geterror().c_str()));
788         } else {
789             std::cerr << "Added image " << filename << ": "
790 << newimage->spec().width << " x " << newimage->spec().height << "\n";
791         }
792         return;
793     }
794 #endif
795     if (m_images.size() == 1) {
796         // If this is the first image, resize to fit it
797         displayCurrentImage();
798         fitWindowToImage(true, true);
799     }
800 }
801 
802 
803 
804 void
saveAs()805 ImageViewer::saveAs()
806 {
807     IvImage* img = cur();
808     if (!img)
809         return;
810     QString name;
811     name = QFileDialog::getSaveFileName(this, tr("Save Image"),
812                                         QString(img->name().c_str()),
813                                         tr(s_file_filters));
814     if (name.isEmpty())
815         return;
816     bool ok = img->write(name.toStdString(), "", image_progress_callback, this);
817     if (!ok) {
818         std::cerr << "Save failed: " << img->geterror() << "\n";
819     }
820 }
821 
822 
823 
824 void
saveWindowAs()825 ImageViewer::saveWindowAs()
826 {
827     IvImage* img = cur();
828     if (!img)
829         return;
830     QString name;
831     name = QFileDialog::getSaveFileName(this, tr("Save Window"),
832                                         QString(img->name().c_str()));
833     if (name.isEmpty())
834         return;
835     img->write(name.toStdString(), "", image_progress_callback, this);  // FIXME
836 }
837 
838 
839 
840 void
saveSelectionAs()841 ImageViewer::saveSelectionAs()
842 {
843     IvImage* img = cur();
844     if (!img)
845         return;
846     QString name;
847     name = QFileDialog::getSaveFileName(this, tr("Save Selection"),
848                                         QString(img->name().c_str()));
849     if (name.isEmpty())
850         return;
851     img->write(name.toStdString(), "", image_progress_callback, this);  // FIXME
852 }
853 
854 
855 
856 void
updateTitle()857 ImageViewer::updateTitle()
858 {
859     IvImage* img = cur();
860     if (!img) {
861         setWindowTitle(tr("iv Image Viewer (no image loaded)"));
862         return;
863     }
864     std::string message;
865     message = Strutil::sprintf("%s - iv Image Viewer", img->name().c_str());
866     setWindowTitle(QString::fromLocal8Bit(message.c_str()));
867 }
868 
869 
870 
871 void
updateStatusBar()872 ImageViewer::updateStatusBar()
873 {
874     const ImageSpec* spec = curspec();
875     if (!spec) {
876         statusImgInfo->setText(tr("No image loaded"));
877         statusViewInfo->setText(tr(""));
878         return;
879     }
880     std::string message;
881     message = Strutil::sprintf("(%d/%d) : ", m_current_image + 1,
882                                (int)m_images.size());
883     message += cur()->shortinfo();
884     statusImgInfo->setText(message.c_str());
885 
886     message.clear();
887     switch (m_color_mode) {
888     case RGBA:
889         message = Strutil::sprintf("RGBA (%d-%d)", m_current_channel,
890                                    m_current_channel + 3);
891         break;
892     case RGB:
893         message = Strutil::sprintf("RGB (%d-%d)", m_current_channel,
894                                    m_current_channel + 2);
895         break;
896     case LUMINANCE:
897         message = Strutil::sprintf("Lum (%d-%d)", m_current_channel,
898                                    m_current_channel + 2);
899         break;
900     case HEATMAP: message = "Heat ";
901     case SINGLE_CHANNEL:
902         if ((int)spec->channelnames.size() > m_current_channel
903             && spec->channelnames[m_current_channel].size())
904             message += spec->channelnames[m_current_channel];
905         else if (m_color_mode == HEATMAP) {
906             message += Strutil::sprintf("%d", m_current_channel);
907         } else {
908             message = Strutil::sprintf("chan %d", m_current_channel);
909         }
910         break;
911     }
912     message += Strutil::sprintf("  %g:%g  exp %+.1f  gam %.2f",
913                                 zoom() >= 1 ? zoom() : 1.0f,
914                                 zoom() >= 1 ? 1.0f : 1.0f / zoom(),
915                                 cur()->exposure(), cur()->gamma());
916     if (cur()->nsubimages() > 1) {
917         if (cur()->auto_subimage()) {
918             message += Strutil::sprintf("  subimg AUTO (%d/%d)",
919                                         cur()->subimage() + 1,
920                                         cur()->nsubimages());
921         } else {
922             message += Strutil::sprintf("  subimg %d/%d", cur()->subimage() + 1,
923                                         cur()->nsubimages());
924         }
925     }
926     if (cur()->nmiplevels() > 1) {
927         message += Strutil::sprintf("  MIP %d/%d", cur()->miplevel() + 1,
928                                     cur()->nmiplevels());
929     }
930 
931     statusViewInfo->setText(message.c_str());  // tr("iv status"));
932 }
933 
934 
935 
936 bool
loadCurrentImage(int subimage,int miplevel)937 ImageViewer::loadCurrentImage(int subimage, int miplevel)
938 {
939     if (m_current_image < 0 || m_current_image >= (int)m_images.size()) {
940         m_current_image = 0;
941     }
942     IvImage* img = cur();
943     if (img) {
944         // We need the spec available to compare the image format with
945         // opengl's capabilities.
946         if (!img->init_spec(img->name(), subimage, miplevel)) {
947             statusImgInfo->setText(
948                 tr("Could not display image: %1.").arg(img->name().c_str()));
949             statusViewInfo->setText(tr(""));
950             return false;
951         }
952 
953         // Used to check whether we'll need to do adjustments in the
954         // CPU. If true, images should be loaded as UINT8.
955         bool allow_transforms = false;
956         bool srgb_transform   = false;
957 
958         // By default, we try to load into OpenGL with the same format,
959         TypeDesc read_format        = TypeDesc::UNKNOWN;
960         const ImageSpec& image_spec = img->spec();
961 
962         if (image_spec.format.basetype == TypeDesc::DOUBLE) {
963             // AFAIK, OpenGL doesn't support 64-bit floats as pixel size.
964             read_format = TypeDesc::FLOAT;
965         }
966         if (glwin->is_glsl_capable()) {
967             if (image_spec.format.basetype == TypeDesc::HALF
968                 && !glwin->is_half_capable()) {
969                 //std::cerr << "Loading HALF-FLOAT as FLOAT\n";
970                 read_format = TypeDesc::FLOAT;
971             }
972             if (IsSpecSrgb(image_spec) && !glwin->is_srgb_capable()) {
973                 // If the image is in sRGB, but OpenGL can't load sRGB textures then
974                 // we'll need to do the transformation on the CPU after loading the
975                 // image. We (so far) can only do this with UINT8 images, so make
976                 // sure that it gets loaded in this format.
977                 //std::cerr << "Loading as UINT8 to do sRGB\n";
978                 read_format      = TypeDesc::UINT8;
979                 srgb_transform   = true;
980                 allow_transforms = true;
981             }
982         } else {
983             //std::cerr << "Loading as UINT8\n";
984             read_format      = TypeDesc::UINT8;
985             allow_transforms = true;
986 
987             if (IsSpecSrgb(image_spec) && !glwin->is_srgb_capable())
988                 srgb_transform = true;
989         }
990 
991         // FIXME: This actually won't work since the ImageCacheFile has already
992         // been created when we did the init_spec.
993         // Check whether IvGL recommends generating mipmaps for this image.
994         //ImageCache *imagecache = ImageCache::create (true);
995         //if (glwin->is_too_big (img) && autoMipmap->isChecked ()) {
996         //    imagecache->attribute ("automip", 1);
997         //} else {
998         //    imagecache->attribute ("automip", 0);
999         //}
1000 
1001         // Read the image from disk or from the ImageCache if available.
1002         if (img->read_iv(subimage, miplevel, false, read_format,
1003                          image_progress_callback, this, allow_transforms)) {
1004             // The image was read successfully.
1005             // Check if we've got to do sRGB to linear (ie, when not supported
1006             // by OpenGL).
1007             // Do the first pixel transform to fill-in the secondary image
1008             // buffer.
1009             if (allow_transforms) {
1010                 img->pixel_transform(srgb_transform, (int)current_color_mode(),
1011                                      current_channel());
1012             }
1013             return true;
1014         } else {
1015             statusImgInfo->setText(
1016                 tr("Could not display image: %1.").arg(img->name().c_str()));
1017             statusViewInfo->setText(tr(""));
1018             return false;
1019         }
1020     }
1021     return false;
1022 }
1023 
1024 
1025 
1026 void
displayCurrentImage(bool update)1027 ImageViewer::displayCurrentImage(bool update)
1028 {
1029     if (m_current_image < 0 || m_current_image >= (int)m_images.size())
1030         m_current_image = 0;
1031     IvImage* img = cur();
1032     if (img) {
1033         if (!img->image_valid()) {
1034             bool load_result = false;
1035 
1036             statusViewInfo->hide();
1037             statusProgress->show();
1038             load_result = loadCurrentImage(std::max(0, img->subimage()),
1039                                            std::max(0, img->miplevel()));
1040             statusProgress->hide();
1041             statusViewInfo->show();
1042 
1043             if (load_result) {
1044                 update = true;
1045             } else {
1046                 return;
1047             }
1048         }
1049     } else {
1050         m_current_image = m_last_image = -1;
1051         ((QOpenGLWidget*)(glwin))->update();
1052     }
1053 
1054     if (update) {
1055         glwin->update();
1056     }
1057     float z = zoom();
1058     if (fitImageToWindowAct->isChecked())
1059         z = zoom_needed_to_fit(glwin->width(), glwin->height());
1060     zoom(z);
1061     //    glwin->trigger_redraw ();
1062 
1063     updateTitle();
1064     updateStatusBar();
1065     if (infoWindow)
1066         infoWindow->update(img);
1067 
1068     //    printAct->setEnabled(true);
1069     //    fitImageToWindowAct->setEnabled(true);
1070     //    fullScreenAct->setEnabled(true);
1071     updateActions();
1072 }
1073 
1074 
1075 
1076 void
deleteCurrentImage()1077 ImageViewer::deleteCurrentImage()
1078 {
1079     IvImage* img = cur();
1080     if (img) {
1081         const char* filename = img->name().c_str();
1082         QString message("Are you sure you want to remove <b>");
1083         message = message + QString(filename) + QString("</b> file from disk?");
1084         QMessageBox::StandardButton button;
1085         button = QMessageBox::question(this, "", message,
1086                                        QMessageBox::Yes | QMessageBox::No);
1087         if (button == QMessageBox::Yes) {
1088             closeImg();
1089             int r = remove(filename);
1090             if (r)
1091                 QMessageBox::information(this, "", "Unable to delete file");
1092         }
1093     }
1094 }
1095 
1096 
1097 
1098 void
current_image(int newimage)1099 ImageViewer::current_image(int newimage)
1100 {
1101 #ifndef NDEBUG
1102     Timer swap_image_time;
1103     swap_image_time.start();
1104 #endif
1105     if (m_images.empty() || newimage < 0 || newimage >= (int)m_images.size())
1106         m_current_image = 0;
1107     if (m_current_image != newimage) {
1108         m_last_image    = (m_current_image >= 0) ? m_current_image : newimage;
1109         m_current_image = newimage;
1110         displayCurrentImage();
1111     } else {
1112         displayCurrentImage(false);
1113     }
1114 #ifndef NDEBUG
1115     swap_image_time.stop();
1116     std::cerr << "Current Image change elapsed time: " << swap_image_time()
1117               << " seconds \n";
1118 #endif
1119 }
1120 
1121 
1122 
1123 void
prevImage()1124 ImageViewer::prevImage()
1125 {
1126     if (m_images.empty())
1127         return;
1128     if (m_current_image == 0)
1129         current_image((int)m_images.size() - 1);
1130     else
1131         current_image(current_image() - 1);
1132 }
1133 
1134 
1135 void
nextImage()1136 ImageViewer::nextImage()
1137 {
1138     if (m_images.empty())
1139         return;
1140     if (m_current_image >= (int)m_images.size() - 1)
1141         current_image(0);
1142     else
1143         current_image(current_image() + 1);
1144 }
1145 
1146 
1147 
1148 void
toggleImage()1149 ImageViewer::toggleImage()
1150 {
1151     current_image(m_last_image);
1152 }
1153 
1154 
1155 
1156 void
exposureMinusOneTenthStop()1157 ImageViewer::exposureMinusOneTenthStop()
1158 {
1159     if (m_images.empty())
1160         return;
1161     IvImage* img = m_images[m_current_image];
1162     img->exposure(img->exposure() - 0.1);
1163     if (!glwin->is_glsl_capable()) {
1164         bool srgb_transform = (!glwin->is_srgb_capable()
1165                                && IsSpecSrgb(img->spec()));
1166         img->pixel_transform(srgb_transform, (int)current_color_mode(),
1167                              current_channel());
1168         displayCurrentImage();
1169     } else {
1170         displayCurrentImage(false);
1171     }
1172 }
1173 
1174 
1175 void
exposureMinusOneHalfStop()1176 ImageViewer::exposureMinusOneHalfStop()
1177 {
1178     if (m_images.empty())
1179         return;
1180     IvImage* img = m_images[m_current_image];
1181     img->exposure(img->exposure() - 0.5);
1182     if (!glwin->is_glsl_capable()) {
1183         bool srgb_transform = (!glwin->is_srgb_capable()
1184                                && IsSpecSrgb(img->spec()));
1185         img->pixel_transform(srgb_transform, (int)current_color_mode(),
1186                              current_channel());
1187         displayCurrentImage();
1188     } else {
1189         displayCurrentImage(false);
1190     }
1191 }
1192 
1193 
1194 void
exposurePlusOneTenthStop()1195 ImageViewer::exposurePlusOneTenthStop()
1196 {
1197     if (m_images.empty())
1198         return;
1199     IvImage* img = m_images[m_current_image];
1200     img->exposure(img->exposure() + 0.1);
1201     if (!glwin->is_glsl_capable()) {
1202         bool srgb_transform = (!glwin->is_srgb_capable()
1203                                && IsSpecSrgb(img->spec()));
1204         img->pixel_transform(srgb_transform, (int)current_color_mode(),
1205                              current_channel());
1206         displayCurrentImage();
1207     } else {
1208         displayCurrentImage(false);
1209     }
1210 }
1211 
1212 
1213 void
exposurePlusOneHalfStop()1214 ImageViewer::exposurePlusOneHalfStop()
1215 {
1216     if (m_images.empty())
1217         return;
1218     IvImage* img = m_images[m_current_image];
1219     img->exposure(img->exposure() + 0.5);
1220     if (!glwin->is_glsl_capable()) {
1221         bool srgb_transform = (!glwin->is_srgb_capable()
1222                                && IsSpecSrgb(img->spec()));
1223         img->pixel_transform(srgb_transform, (int)current_color_mode(),
1224                              current_channel());
1225         displayCurrentImage();
1226     } else {
1227         displayCurrentImage(false);
1228     }
1229 }
1230 
1231 
1232 
1233 void
gammaMinus()1234 ImageViewer::gammaMinus()
1235 {
1236     if (m_images.empty())
1237         return;
1238     IvImage* img = m_images[m_current_image];
1239     img->gamma(img->gamma() - 0.05);
1240     if (!glwin->is_glsl_capable()) {
1241         bool srgb_transform = (!glwin->is_srgb_capable()
1242                                && IsSpecSrgb(img->spec()));
1243         img->pixel_transform(srgb_transform, (int)current_color_mode(),
1244                              current_channel());
1245         displayCurrentImage();
1246     } else {
1247         displayCurrentImage(false);
1248     }
1249 }
1250 
1251 
1252 void
gammaPlus()1253 ImageViewer::gammaPlus()
1254 {
1255     if (m_images.empty())
1256         return;
1257     IvImage* img = m_images[m_current_image];
1258     img->gamma(img->gamma() + 0.05);
1259     if (!glwin->is_glsl_capable()) {
1260         bool srgb_transform = (!glwin->is_srgb_capable()
1261                                && IsSpecSrgb(img->spec()));
1262         img->pixel_transform(srgb_transform, (int)current_color_mode(),
1263                              current_channel());
1264         displayCurrentImage();
1265     } else {
1266         displayCurrentImage(false);
1267     }
1268 }
1269 
1270 
1271 
1272 void
slide(long,bool b)1273 ImageViewer::slide(long /*t*/, bool b)
1274 {
1275     slideLoopAct->setChecked(b == true);
1276     slideNoLoopAct->setChecked(b == false);
1277 }
1278 
1279 
1280 
1281 void
viewChannel(int c,COLOR_MODE colormode)1282 ImageViewer::viewChannel(int c, COLOR_MODE colormode)
1283 {
1284 #ifndef NDEBUG
1285     Timer change_channel_time;
1286     change_channel_time.start();
1287 #endif
1288     if (m_current_channel != c || colormode != m_color_mode) {
1289         bool update = true;
1290         if (!glwin->is_glsl_capable()) {
1291             IvImage* img = cur();
1292             if (img) {
1293                 bool srgb_transform = (!glwin->is_srgb_capable()
1294                                        && IsSpecSrgb(img->spec()));
1295                 img->pixel_transform(srgb_transform, (int)colormode, c);
1296             }
1297         } else {
1298             // FIXME: There are even more chances to avoid updating the textures
1299             // if we can keep trac of which channels are in the texture.
1300             if (m_current_channel == c) {
1301                 if (m_color_mode == SINGLE_CHANNEL || m_color_mode == HEATMAP) {
1302                     if (colormode == HEATMAP || colormode == SINGLE_CHANNEL)
1303                         update = false;
1304                 } else if (m_color_mode == RGB || m_color_mode == LUMINANCE) {
1305                     if (colormode == RGB || colormode == LUMINANCE)
1306                         update = false;
1307                 }
1308             }
1309         }
1310         m_current_channel = c;
1311         m_color_mode      = colormode;
1312         displayCurrentImage(update);
1313 
1314         viewChannelFullAct->setChecked(c == 0 && m_color_mode == RGBA);
1315         viewChannelRedAct->setChecked(c == 0 && m_color_mode == SINGLE_CHANNEL);
1316         viewChannelGreenAct->setChecked(c == 1
1317                                         && m_color_mode == SINGLE_CHANNEL);
1318         viewChannelBlueAct->setChecked(c == 2
1319                                        && m_color_mode == SINGLE_CHANNEL);
1320         viewChannelAlphaAct->setChecked(c == 3
1321                                         && m_color_mode == SINGLE_CHANNEL);
1322         viewColorLumAct->setChecked(m_color_mode == LUMINANCE);
1323         viewColorRGBAAct->setChecked(m_color_mode == RGBA);
1324         viewColorRGBAct->setChecked(m_color_mode == RGB);
1325         viewColor1ChAct->setChecked(m_color_mode == SINGLE_CHANNEL);
1326         viewColorHeatmapAct->setChecked(m_color_mode == HEATMAP);
1327     }
1328 #ifndef NDEBUG
1329     change_channel_time.stop();
1330     std::cerr << "Change channel elapsed time: " << change_channel_time()
1331               << " seconds \n";
1332 #endif
1333 }
1334 
1335 
1336 void
slideImages()1337 ImageViewer::slideImages()
1338 {
1339     if (m_images.empty())
1340         return;
1341     if (m_current_image >= (int)m_images.size() - 1) {
1342         if (slide_loop == true)
1343             current_image(0);
1344         else {
1345             slideTimer->stop();
1346             disconnect(slideTimer, 0, 0, 0);
1347         }
1348     } else
1349         current_image(current_image() + 1);
1350 }
1351 
1352 
1353 void
slideShow()1354 ImageViewer::slideShow()
1355 {
1356     fullScreenToggle();
1357     connect(slideTimer, SIGNAL(timeout()), this, SLOT(slideImages()));
1358     slideTimer->start(slideDuration_ms);
1359     updateActions();
1360 }
1361 
1362 
1363 
1364 void
slideLoop()1365 ImageViewer::slideLoop()
1366 {
1367     slide_loop = true;
1368     slide(slideDuration_ms, slide_loop);
1369 }
1370 
1371 
1372 void
slideNoLoop()1373 ImageViewer::slideNoLoop()
1374 {
1375     slide_loop = false;
1376     slide(slideDuration_ms, slide_loop);
1377 }
1378 
1379 
1380 void
setSlideShowDuration(int seconds)1381 ImageViewer::setSlideShowDuration(int seconds)
1382 {
1383     slideDuration_ms = seconds * 1000;
1384 }
1385 
1386 
1387 
1388 static bool
compName(IvImage * first,IvImage * second)1389 compName(IvImage* first, IvImage* second)
1390 {
1391     std::string firstFile  = Filesystem::filename(first->name());
1392     std::string secondFile = Filesystem::filename(second->name());
1393     return (firstFile.compare(secondFile) < 0);
1394 }
1395 
1396 
1397 
1398 void
sortByName()1399 ImageViewer::sortByName()
1400 {
1401     int numImg = m_images.size();
1402     if (numImg < 2)
1403         return;
1404     std::sort(m_images.begin(), m_images.end(), &compName);
1405     current_image(0);
1406     displayCurrentImage();
1407     // updateActions();
1408 }
1409 
1410 
1411 
1412 static bool
compPath(IvImage * first,IvImage * second)1413 compPath(IvImage* first, IvImage* second)
1414 {
1415     std::string firstFile  = first->name();
1416     std::string secondFile = second->name();
1417     return (firstFile.compare(secondFile) < 0);
1418 }
1419 
1420 
1421 
1422 void
sortByPath()1423 ImageViewer::sortByPath()
1424 {
1425     int numImg = m_images.size();
1426     if (numImg < 2)
1427         return;
1428     std::sort(m_images.begin(), m_images.end(), &compPath);
1429     current_image(0);
1430     displayCurrentImage();
1431     // updateActions();
1432 }
1433 
1434 
1435 
1436 static bool
DateTime_to_time_t(const char * datetime,time_t & timet)1437 DateTime_to_time_t(const char* datetime, time_t& timet)
1438 {
1439     int year, month, day, hour, min, sec;
1440     int r = sscanf(datetime, "%d:%d:%d %d:%d:%d", &year, &month, &day, &hour,
1441                    &min, &sec);
1442     // printf ("%d  %d:%d:%d %d:%d:%d\n", r, year, month, day, hour, min, sec);
1443     if (r != 6)
1444         return false;
1445     struct tm tmtime;
1446     time_t now;
1447     Sysutil::get_local_time(&now, &tmtime);  // fill in defaults
1448     tmtime.tm_sec  = sec;
1449     tmtime.tm_min  = min;
1450     tmtime.tm_hour = hour;
1451     tmtime.tm_mday = day;
1452     tmtime.tm_mon  = month - 1;
1453     tmtime.tm_year = year - 1900;
1454     timet          = mktime(&tmtime);
1455     return true;
1456 }
1457 
1458 
1459 
1460 static bool
compImageDate(IvImage * first,IvImage * second)1461 compImageDate(IvImage* first, IvImage* second)
1462 {
1463     std::time_t firstFile  = time(NULL);
1464     std::time_t secondFile = time(NULL);
1465     double diff;
1466     std::string metadatatime = first->spec().get_string_attribute("DateTime");
1467     if (metadatatime.empty()) {
1468         if (first->init_spec(first->name(), 0, 0)) {
1469             metadatatime = first->spec().get_string_attribute("DateTime");
1470             if (metadatatime.empty()) {
1471                 if (!Filesystem::exists(first->name()))
1472                     return false;
1473                 firstFile = Filesystem::last_write_time(first->name());
1474             }
1475         } else
1476             return false;
1477     }
1478     DateTime_to_time_t(metadatatime.c_str(), firstFile);
1479     metadatatime = second->spec().get_string_attribute("DateTime");
1480     if (metadatatime.empty()) {
1481         if (second->init_spec(second->name(), 0, 0)) {
1482             metadatatime = second->spec().get_string_attribute("DateTime");
1483             if (metadatatime.empty()) {
1484                 if (!Filesystem::exists(second->name()))
1485                     return true;
1486                 secondFile = Filesystem::last_write_time(second->name());
1487             }
1488         } else
1489             return true;
1490     }
1491     DateTime_to_time_t(metadatatime.c_str(), secondFile);
1492     diff = difftime(firstFile, secondFile);
1493     if (diff == 0)
1494         return compName(first, second);
1495     return (diff < 0);
1496 }
1497 
1498 
1499 
1500 void
sortByImageDate()1501 ImageViewer::sortByImageDate()
1502 {
1503     int numImg = m_images.size();
1504     if (numImg < 2)
1505         return;
1506     std::sort(m_images.begin(), m_images.end(), &compImageDate);
1507     current_image(0);
1508     displayCurrentImage();
1509     // updateActions();
1510 }
1511 
1512 
1513 
1514 static bool
compFileDate(IvImage * first,IvImage * second)1515 compFileDate(IvImage* first, IvImage* second)
1516 {
1517     std::time_t firstFile, secondFile;
1518     double diff;
1519     if (!Filesystem::exists(first->name()))
1520         return false;
1521     firstFile = Filesystem::last_write_time(first->name());
1522     if (!Filesystem::exists(second->name()))
1523         return true;
1524     secondFile = Filesystem::last_write_time(second->name());
1525     diff       = difftime(firstFile, secondFile);
1526     if (diff == 0)
1527         return compName(first, second);
1528     return (diff < 0);
1529 }
1530 
1531 
1532 
1533 void
sortByFileDate()1534 ImageViewer::sortByFileDate()
1535 {
1536     int numImg = m_images.size();
1537     if (numImg < 2)
1538         return;
1539     std::sort(m_images.begin(), m_images.end(), &compFileDate);
1540     current_image(0);
1541     displayCurrentImage();
1542     // updateActions();
1543 }
1544 
1545 
1546 
1547 void
sortReverse()1548 ImageViewer::sortReverse()
1549 {
1550     int numImg = m_images.size();
1551     if (numImg < 2)
1552         return;
1553     std::reverse(m_images.begin(), m_images.end());
1554     current_image(0);
1555     displayCurrentImage();
1556     // updateActions();
1557 }
1558 
1559 
1560 
1561 void
viewChannelFull()1562 ImageViewer::viewChannelFull()
1563 {
1564     viewChannel(0, RGBA);
1565 }
1566 
1567 
1568 void
viewChannelRed()1569 ImageViewer::viewChannelRed()
1570 {
1571     viewChannel(0, SINGLE_CHANNEL);
1572 }
1573 
1574 
1575 void
viewChannelGreen()1576 ImageViewer::viewChannelGreen()
1577 {
1578     viewChannel(1, SINGLE_CHANNEL);
1579 }
1580 
1581 
1582 void
viewChannelBlue()1583 ImageViewer::viewChannelBlue()
1584 {
1585     viewChannel(2, SINGLE_CHANNEL);
1586 }
1587 
1588 
1589 void
viewChannelAlpha()1590 ImageViewer::viewChannelAlpha()
1591 {
1592     viewChannel(3, SINGLE_CHANNEL);
1593 }
1594 
1595 
1596 void
viewChannelLuminance()1597 ImageViewer::viewChannelLuminance()
1598 {
1599     viewChannel(m_current_channel, LUMINANCE);
1600 }
1601 
1602 
1603 void
viewColorRGBA()1604 ImageViewer::viewColorRGBA()
1605 {
1606     viewChannel(m_current_channel, RGBA);
1607 }
1608 
1609 
1610 void
viewColorRGB()1611 ImageViewer::viewColorRGB()
1612 {
1613     viewChannel(m_current_channel, RGB);
1614 }
1615 
1616 
1617 void
viewColor1Ch()1618 ImageViewer::viewColor1Ch()
1619 {
1620     viewChannel(m_current_channel, SINGLE_CHANNEL);
1621 }
1622 
1623 
1624 void
viewColorHeatmap()1625 ImageViewer::viewColorHeatmap()
1626 {
1627     viewChannel(m_current_channel, HEATMAP);
1628 }
1629 
1630 
1631 void
viewChannelPrev()1632 ImageViewer::viewChannelPrev()
1633 {
1634     if (glwin->is_glsl_capable()) {
1635         if (m_current_channel > 0)
1636             viewChannel(m_current_channel - 1, m_color_mode);
1637     } else {
1638         // Simulate old behavior.
1639         if (m_color_mode == RGBA || m_color_mode == RGB) {
1640             viewChannel(m_current_channel, LUMINANCE);
1641         } else if (m_color_mode == SINGLE_CHANNEL) {
1642             if (m_current_channel == 0)
1643                 viewChannelFull();
1644             else
1645                 viewChannel(m_current_channel - 1, SINGLE_CHANNEL);
1646         }
1647     }
1648 }
1649 
1650 
1651 void
viewChannelNext()1652 ImageViewer::viewChannelNext()
1653 {
1654     if (glwin->is_glsl_capable()) {
1655         viewChannel(m_current_channel + 1, m_color_mode);
1656     } else {
1657         // Simulate old behavior.
1658         if (m_color_mode == LUMINANCE) {
1659             viewChannelFull();
1660         } else if (m_color_mode == RGBA || m_color_mode == RGB) {
1661             viewChannelRed();
1662         } else if (m_color_mode == SINGLE_CHANNEL) {
1663             viewChannel(m_current_channel + 1, SINGLE_CHANNEL);
1664         }
1665     }
1666 }
1667 
1668 
1669 
1670 void
viewSubimagePrev()1671 ImageViewer::viewSubimagePrev()
1672 {
1673     IvImage* img = cur();
1674     if (!img)
1675         return;
1676     bool ok = false;
1677     if (img->miplevel() > 0) {
1678         ok = loadCurrentImage(img->subimage(), img->miplevel() - 1);
1679     } else if (img->subimage() > 0) {
1680         ok = loadCurrentImage(img->subimage() - 1);
1681     } else if (img->nsubimages() > 0) {
1682         img->auto_subimage(true);
1683         ok = loadCurrentImage(0);
1684     }
1685     if (ok) {
1686         if (fitImageToWindowAct->isChecked())
1687             fitImageToWindow();
1688         displayCurrentImage();
1689     }
1690 }
1691 
1692 
1693 void
viewSubimageNext()1694 ImageViewer::viewSubimageNext()
1695 {
1696     IvImage* img = cur();
1697     if (!img)
1698         return;
1699     bool ok = false;
1700     if (img->auto_subimage()) {
1701         img->auto_subimage(false);
1702         ok = loadCurrentImage(0);
1703     } else if (img->miplevel() < img->nmiplevels() - 1) {
1704         ok = loadCurrentImage(img->subimage(), img->miplevel() + 1);
1705     } else if (img->subimage() < img->nsubimages() - 1) {
1706         ok = loadCurrentImage(img->subimage() + 1);
1707     }
1708     if (ok) {
1709         if (fitImageToWindowAct->isChecked())
1710             fitImageToWindow();
1711         displayCurrentImage();
1712     }
1713 }
1714 
1715 
1716 
1717 void
keyPressEvent(QKeyEvent * event)1718 ImageViewer::keyPressEvent(QKeyEvent* event)
1719 {
1720     switch (event->key()) {
1721     case Qt::Key_Left:
1722     case Qt::Key_Up:
1723     case Qt::Key_PageUp: prevImage(); return;  //break;
1724     case Qt::Key_Right:
1725         //        std::cerr << "Modifier is " << (int)event->modifiers() << '\n';
1726         //        fprintf (stderr, "%x\n", (int)event->modifiers());
1727         //        if (event->modifiers() & Qt::ShiftModifier)
1728         //            std::cerr << "hey, ctrl right\n";
1729     case Qt::Key_Down:
1730     case Qt::Key_PageDown: nextImage(); return;  //break;
1731     case Qt::Key_Escape:
1732         if (m_fullscreen)
1733             fullScreenToggle();
1734         return;
1735     case Qt::Key_Minus:
1736     case Qt::Key_Underscore: zoomOut(); break;
1737     case Qt::Key_Plus:
1738     case Qt::Key_Equal: zoomIn(); break;
1739     default:
1740         // std::cerr << "ImageViewer key " << (int)event->key() << '\n';
1741         QMainWindow::keyPressEvent(event);
1742     }
1743 }
1744 
1745 
1746 
1747 void
resizeEvent(QResizeEvent * event)1748 ImageViewer::resizeEvent(QResizeEvent* event)
1749 {
1750     if (fitImageToWindowAct->isChecked())
1751         fitImageToWindow();
1752     QMainWindow::resizeEvent(event);
1753 }
1754 
1755 
1756 
1757 void
closeImg()1758 ImageViewer::closeImg()
1759 {
1760     if (m_images.empty())
1761         return;
1762     delete m_images[m_current_image];
1763     m_images[m_current_image] = NULL;
1764     m_images.erase(m_images.begin() + m_current_image);
1765 
1766     // Update image indices
1767     // This should be done for all image indices we may be storing
1768     if (m_last_image == m_current_image) {
1769         if (!m_images.empty() && m_last_image > 0)
1770             m_last_image = 0;
1771         else
1772             m_last_image = -1;
1773     }
1774     if (m_last_image > m_current_image)
1775         m_last_image--;
1776 
1777     m_current_image = m_current_image < (int)m_images.size() ? m_current_image
1778                                                              : 0;
1779     displayCurrentImage();
1780 }
1781 
1782 
1783 
1784 void
print()1785 ImageViewer::print()
1786 {
1787 #if 0
1788     Q_ASSERT(imageLabel->pixmap());
1789     QPrintDialog dialog(&printer, this);
1790     if (dialog.exec()) {
1791         QPainter painter(&printer);
1792         QRect rect = painter.viewport();
1793         QSize size = imageLabel->pixmap()->size();
1794         size.scale(rect.size(), Qt::KeepAspectRatio);
1795         painter.setViewport(rect.x(), rect.y(), size.width(), size.height());
1796         painter.setWindow(imageLabel->pixmap()->rect());
1797         painter.drawPixmap(0, 0, *imageLabel->pixmap());
1798     }
1799 #endif
1800 }
1801 
1802 
1803 
1804 void
zoomIn()1805 ImageViewer::zoomIn()
1806 {
1807     IvImage* img = cur();
1808     if (!img)
1809         return;
1810     if (zoom() >= 64)
1811         return;
1812     float oldzoom = zoom();
1813     float newzoom = ceil2f(oldzoom);
1814 
1815     float xc, yc;  // Center view position
1816     glwin->get_center(xc, yc);
1817     int xm, ym;  // Mouse position
1818     glwin->get_focus_image_pixel(xm, ym);
1819     float xoffset      = xc - xm;
1820     float yoffset      = yc - ym;
1821     float maxzoomratio = std::max(oldzoom / newzoom, newzoom / oldzoom);
1822     int nsteps         = (int)OIIO::clamp(20 * (maxzoomratio - 1), 2.0f, 10.0f);
1823     for (int i = 1; i <= nsteps; ++i) {
1824         float a         = (float)i / (float)nsteps;  // Interpolation amount
1825         float z         = OIIO::lerp(oldzoom, newzoom, a);
1826         float zoomratio = z / oldzoom;
1827         view(xm + xoffset / zoomratio, ym + yoffset / zoomratio, z, false);
1828         if (i != nsteps) {
1829             QApplication::processEvents();
1830             Sysutil::usleep(1000000 / 4 / nsteps);
1831         }
1832     }
1833 
1834     fitImageToWindowAct->setChecked(false);
1835 }
1836 
1837 
1838 
1839 void
zoomOut()1840 ImageViewer::zoomOut()
1841 {
1842     IvImage* img = cur();
1843     if (!img)
1844         return;
1845     if (zoom() <= 1.0f / 64)
1846         return;
1847     float oldzoom = zoom();
1848     float newzoom = floor2f(oldzoom);
1849 
1850     float xcpel, ycpel;  // Center view position
1851     glwin->get_center(xcpel, ycpel);
1852     int xmpel, ympel;  // Mouse position
1853     glwin->get_focus_image_pixel(xmpel, ympel);
1854     float xoffset      = xcpel - xmpel;
1855     float yoffset      = ycpel - ympel;
1856     float maxzoomratio = std::max(oldzoom / newzoom, newzoom / oldzoom);
1857     int nsteps         = (int)OIIO::clamp(20 * (maxzoomratio - 1), 2.0f, 10.0f);
1858     for (int i = 1; i <= nsteps; ++i) {
1859         float a         = (float)i / (float)nsteps;  // Interpolation amount
1860         float z         = OIIO::lerp(oldzoom, newzoom, a);
1861         float zoomratio = z / oldzoom;
1862         view(xmpel + xoffset / zoomratio, ympel + yoffset / zoomratio, z,
1863              false);
1864         if (i != nsteps) {
1865             QApplication::processEvents();
1866             Sysutil::usleep(1000000 / 4 / nsteps);
1867         }
1868     }
1869 
1870     fitImageToWindowAct->setChecked(false);
1871 }
1872 
1873 
1874 void
normalSize()1875 ImageViewer::normalSize()
1876 {
1877     IvImage* img = cur();
1878     if (!img)
1879         return;
1880     fitImageToWindowAct->setChecked(false);
1881     float xcenter = img->oriented_full_x() + 0.5 * img->oriented_full_width();
1882     float ycenter = img->oriented_full_y() + 0.5 * img->oriented_full_height();
1883     view(xcenter, ycenter, 1.0, true);
1884     fitWindowToImage(false);
1885 }
1886 
1887 
1888 
1889 float
zoom_needed_to_fit(int w,int h)1890 ImageViewer::zoom_needed_to_fit(int w, int h)
1891 {
1892     IvImage* img = cur();
1893     if (!img)
1894         return 1;
1895     float zw = (float)w / img->oriented_width();
1896     float zh = (float)h / img->oriented_height();
1897     return std::min(zw, zh);
1898 }
1899 
1900 
1901 
1902 void
fitImageToWindow()1903 ImageViewer::fitImageToWindow()
1904 {
1905     IvImage* img = cur();
1906     if (!img)
1907         return;
1908     fitImageToWindowAct->setChecked(true);
1909     zoom(zoom_needed_to_fit(glwin->width(), glwin->height()));
1910 }
1911 
1912 
1913 
1914 void
fitWindowToImage(bool zoomok,bool minsize)1915 ImageViewer::fitWindowToImage(bool zoomok, bool minsize)
1916 {
1917     IvImage* img = cur();
1918     // Don't resize when there's no image or the image hasn't been opened yet
1919     // (or we failed to open it).
1920     if (!img || !img->image_valid())
1921         return;
1922         // FIXME -- figure out a way to make it exactly right, even for the
1923         // main window border, etc.
1924 #ifdef __APPLE__
1925     int extraw = 0;  //12; // width() - minimumWidth();
1926     int extrah = statusBar()->height()
1927                  + 0;  //40; // height() - minimumHeight();
1928 #else
1929     int extraw = 4;  //12; // width() - minimumWidth();
1930     int extrah = statusBar()->height()
1931                  + 4;  //40; // height() - minimumHeight();
1932 #endif
1933     //    std::cerr << "extra wh = " << extraw << ' ' << extrah << '\n';
1934 
1935     float z = zoom();
1936     int w   = (int)(img->oriented_full_width() * z) + extraw;
1937     int h   = (int)(img->oriented_full_height() * z) + extrah;
1938     if (minsize) {
1939         if (w < m_default_width) {
1940             w = m_default_width;
1941         }
1942         if (h < m_default_height) {
1943             h = m_default_height;
1944         }
1945     }
1946 
1947     if (!m_fullscreen) {
1948         QDesktopWidget* desktop = QApplication::desktop();
1949         QRect availgeom         = desktop->availableGeometry(this);
1950         int availwidth          = availgeom.width() - extraw - 20;
1951         int availheight = availgeom.height() - extrah - menuBar()->height()
1952                           - 20;
1953 #if 0
1954         QRect screengeom = desktop->screenGeometry (this);
1955         std::cerr << "available desktop geom " << availgeom.x() << ' ' << availgeom.y() << ' ' << availgeom.width() << "x" << availgeom.height() << "\n";
1956         std::cerr << "screen desktop geom " << screengeom.x() << ' ' << screengeom.y() << ' ' << screengeom.width() << "x" << screengeom.height() << "\n";
1957 #endif
1958         if (w > availwidth || h > availheight) {
1959             w = std::min(w, availwidth);
1960             h = std::min(h, availheight);
1961             if (zoomok) {
1962                 z = zoom_needed_to_fit(w, h);
1963                 w = (int)(img->oriented_full_width() * z) + extraw;
1964                 h = (int)(img->oriented_full_height() * z) + extrah;
1965                 // std::cerr << "must rezoom to " << z << " to fit\n";
1966             }
1967             // std::cerr << "New window geom " << w << "x" << h << "\n";
1968             int posx = x(), posy = y();
1969             if (posx + w > availwidth || posy + h > availheight) {
1970                 if (posx + w > availwidth)
1971                     posx = std::max(0, availwidth - w) + availgeom.x();
1972                 if (posy + h > availheight)
1973                     posy = std::max(0, availheight - h) + availgeom.y();
1974                 // std::cerr << "New position " << posx << ' ' << posy << "\n";
1975                 move(QPoint(posx, posy));
1976             }
1977         }
1978     }
1979 
1980     float midx = img->oriented_full_x() + 0.5 * img->oriented_full_width();
1981     float midy = img->oriented_full_y() + 0.5 * img->oriented_full_height();
1982     view(midx, midy, z, false, false);
1983     resize(w, h);  // Resize will trigger a repaint.
1984 
1985 #if 0
1986     QRect g = geometry();
1987     std::cerr << "geom " << g.x() << ' ' << g.y() << ' ' << g.width() << "x" << g.height() << "\n";
1988     g = frameGeometry();
1989     std::cerr << "frame geom " << g.x() << ' ' << g.y() << ' ' << g.width() << "x" << g.height() << "\n";
1990     g = glwin->geometry();
1991     std::cerr << "ogl geom " << g.x() << ' ' << g.y() << ' ' << g.width() << "x" << g.height() << "\n";
1992     std::cerr << "Status bar height = " << statusBar()->height() << "\n";
1993 #endif
1994 
1995 #if 0
1996     bool fit = fitWindowToImageAct->isChecked();
1997     if (!fit) {
1998         normalSize();
1999     }
2000 #endif
2001     updateActions();
2002 }
2003 
2004 
2005 
2006 void
fullScreenToggle()2007 ImageViewer::fullScreenToggle()
2008 {
2009     if (m_fullscreen) {
2010         menuBar()->show();
2011         statusBar()->show();
2012         showNormal();
2013         m_fullscreen = false;
2014         slideTimer->stop();
2015         disconnect(slideTimer, 0, 0, 0);
2016     } else {
2017         menuBar()->hide();
2018         statusBar()->hide();
2019         showFullScreen();
2020         m_fullscreen = true;
2021         fitImageToWindow();
2022     }
2023 }
2024 
2025 
2026 
2027 void
about()2028 ImageViewer::about()
2029 {
2030     QMessageBox::about(
2031         this, tr("About iv"),
2032         tr("<p><b>iv</b> is the image viewer for OpenImageIO.</p>"
2033            "<p>(c) Copyright Contributors to the OpenImageIO project.</p>"
2034            "<p>See <a href='http://openimageio.org'>http://openimageio.org</a> for details.</p>"));
2035 }
2036 
2037 
2038 void
updateActions()2039 ImageViewer::updateActions()
2040 {
2041     //    zoomInAct->setEnabled(!fitImageToWindowAct->isChecked());
2042     //    zoomOutAct->setEnabled(!fitImageToWindowAct->isChecked());
2043     //    normalSizeAct->setEnabled(!fitImageToWindowAct->isChecked());
2044 }
2045 
2046 
2047 
2048 static inline void
calc_subimage_from_zoom(const IvImage * img,int & subimage,float & zoom,float & xcenter,float & ycenter)2049 calc_subimage_from_zoom(const IvImage* img, int& subimage, float& zoom,
2050                         float& xcenter, float& ycenter)
2051 {
2052     int rel_subimage = std::trunc(std::log2(1.0f / zoom));
2053     subimage         = clamp<int>(img->subimage() + rel_subimage, 0,
2054                           img->nsubimages() - 1);
2055     if (!(img->subimage() == 0 && zoom > 1)
2056         && !(img->subimage() == img->nsubimages() - 1 && zoom < 1)) {
2057         float pow_zoom = powf(2.0f, (float)rel_subimage);
2058         zoom *= pow_zoom;
2059         xcenter /= pow_zoom;
2060         ycenter /= pow_zoom;
2061     }
2062 }
2063 
2064 
2065 
2066 void
view(float xcenter,float ycenter,float newzoom,bool smooth,bool redraw)2067 ImageViewer::view(float xcenter, float ycenter, float newzoom, bool smooth,
2068                   bool redraw)
2069 {
2070     IvImage* img = cur();
2071     if (!img)
2072         return;
2073 
2074     float oldzoom = m_zoom;
2075     float oldxcenter, oldycenter;
2076     glwin->get_center(oldxcenter, oldycenter);
2077     float zoomratio = std::max(oldzoom / newzoom, newzoom / oldzoom);
2078     int nsteps      = (int)OIIO::clamp(20 * (zoomratio - 1), 2.0f, 10.0f);
2079     if (!smooth || !redraw)
2080         nsteps = 1;
2081     for (int i = 1; i <= nsteps; ++i) {
2082         float a  = (float)i / (float)nsteps;  // Interpolation amount
2083         float xc = OIIO::lerp(oldxcenter, xcenter, a);
2084         float yc = OIIO::lerp(oldycenter, ycenter, a);
2085         m_zoom   = OIIO::lerp(oldzoom, newzoom, a);
2086 
2087         glwin->view(xc, yc, m_zoom, redraw);  // Triggers redraw automatically
2088         if (i != nsteps) {
2089             QApplication::processEvents();
2090             Sysutil::usleep(1000000 / 4 / nsteps);
2091         }
2092     }
2093 
2094     if (img->auto_subimage()) {
2095         int subimage = 0;
2096         calc_subimage_from_zoom(img, subimage, m_zoom, xcenter, ycenter);
2097         if (subimage != img->subimage()) {
2098             //std::cerr << "Changing to subimage " << subimage;
2099             //std::cerr << " With zoom: " << m_zoom << '\n';
2100             loadCurrentImage(subimage);
2101             glwin->update();
2102             glwin->view(xcenter, ycenter, m_zoom, redraw);
2103         }
2104     }
2105 
2106     //    zoomInAct->setEnabled (zoom() < 64.0);
2107     //    zoomOutAct->setEnabled (zoom() > 1.0/64);
2108 
2109     updateStatusBar();
2110 }
2111 
2112 
2113 
2114 void
zoom(float newzoom,bool smooth)2115 ImageViewer::zoom(float newzoom, bool smooth)
2116 {
2117     float xcenter, ycenter;
2118     glwin->get_center(xcenter, ycenter);
2119     view(xcenter, ycenter, newzoom, smooth);
2120 }
2121 
2122 
2123 
2124 void
showInfoWindow()2125 ImageViewer::showInfoWindow()
2126 {
2127     if (!infoWindow) {
2128         infoWindow = new IvInfoWindow(*this, true);
2129         infoWindow->setPalette(m_palette);
2130     }
2131     infoWindow->update(cur());
2132     if (infoWindow->isHidden())
2133         infoWindow->show();
2134     else
2135         infoWindow->hide();
2136 }
2137 
2138 
2139 
2140 void
showPixelviewWindow()2141 ImageViewer::showPixelviewWindow()
2142 {
2143     ((QOpenGLWidget*)(glwin))->update();
2144 }
2145 
2146 
2147 
2148 void
editPreferences()2149 ImageViewer::editPreferences()
2150 {
2151     if (!preferenceWindow) {
2152         preferenceWindow = new IvPreferenceWindow(*this);
2153         preferenceWindow->setPalette(m_palette);
2154     }
2155     preferenceWindow->show();
2156 }
2157