1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# python-gphoto2 - Python interface to libgphoto2
5# http://github.com/jim-easterbrook/python-gphoto2
6# Copyright (C) 2019  "sdaau"
7#
8# This program is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
21# another camera config gui, with load/save settings to file, and live view
22# started: sdaau 2019, on with python3-gphoto2 and `sudo -H pip2 install gphoto2`, Ubuntu 18.04
23# uses camera-config-gui.py, and code from focus-gui.py, time_lapse.py
24
25"""
26NOTE that properties are reported by the camera, depending on the camera state! On Canon S3 IS:
27For instance, if camera is not in capture mode, then upon --save-cam-conf-json, the properties "imgsettings" and "capturesettings" (which otherwise contain other properties as children) will return error.
28If camera is in capture mode, and prop "shootingmode" is "Auto" - then "iso" has "Auto" option, "exposurecompensation" and "flashcompensation" is read-write, "aperture" and "shutterspeed" is read-only
29If camera is in capture mode, and prop "shootingmode" is "Manual" - then "iso" loses "Auto" option, "exposurecompensation" and "flashcompensation" become read-only, "aperture" and "shutterspeed" become read-write
30NOTE: some properties may take a while to update, see [waiting for variables to update, and wait_for_event causing picture to be taken? · Issue #75 · jim-easterbrook/python-gphoto2 · GitHub](https://github.com/jim-easterbrook/python-gphoto2/issues/75)
31Upon loading json files on camera that change some modes, you might get an error, in which case you can try loading the same file again
32NOTE: if doing live view on Canon, while capture=0, then still an image is returned - but it is the last captured image (except if immediately after camera startup, when capture=0 is ignored, and live view captures regardless). However, sometimes live view may freeze even with capture=1 - in which case, switching Camera Output from Off to LCD/Video Out and back to Off helps.
33NOTE: Switching from viewing full res image capture, to lower resolution live view preview, and vice versa, should automatically fit the view. But if the view gets "lost": in that case, hit Ctrl-F for fit to view once, then can do Ctrl-Z for original scale (or mouse wheel to zoom in/out the preview).
34"""
35
36from __future__ import print_function
37
38import io
39import logging
40import math
41import sys, os
42import re
43import argparse
44import json, codecs
45from collections import OrderedDict
46from datetime import datetime
47import time
48import tzlocal # sudo -H pip install tzlocal # pip2, pip3
49my_timezone = tzlocal.get_localzone()
50
51from PIL import Image, ImageDraw, ImageFont
52
53from PyQt5 import QtCore, QtWidgets, QtGui
54from PyQt5.QtCore import Qt, pyqtSignal, QPoint, QRect
55from PyQt5.QtGui import QIcon
56from PyQt5.QtWidgets import QFileDialog
57
58import gphoto2 as gp
59
60THISSCRIPTDIR = os.path.dirname(os.path.realpath(__file__))
61sys.path.insert(1,THISSCRIPTDIR)
62ccgoo = __import__('camera-config-gui')
63NOCAMIMG = "cam-conf-no-cam.png"
64APPNAME = "cam-conf-view-gui.py"
65
66patTrailDigSpace = re.compile(r'(?<=\.)(\d+?)(0+)(?=[^\d]|$)') # SO: 32348435
67
68# blacklist by properties' names, since on a Canon S3 IS, serialnumber etc are read-write
69# make it react by regex too, as on a Canon S3 IS there is read-write:
70# 'd04a' = '0' (PTP Property 0xd04a)), 'd034' = '1548282664' (UNIX Time)) ...
71# which should likely not be changed; most of ^d0.* properties are read-only or duplicates,
72# but still good to ignore them all in one go:
73BLACKLISTPROPSREGEX = [
74    'opcode', 'datetime', 'serialnumber', 'manufacturer', 'cameramodel', 'deviceversion', 'vendorextension', r'^d0'
75]
76BLACKLISTPROPSREGEX = [re.compile(x) for x in BLACKLISTPROPSREGEX]
77
78PROPNAMESTOSETFIRST = [ "shootingmode" ]
79MINFPS=0.5 # just for the text in status bar
80SLEEPWAITCHANGE=2 # amount of seconds to wait for variable update after first pass, when loading json file
81
82def get_camera_model(camera_config):
83    # get the camera model
84    OK, camera_model = gp.gp_widget_get_child_by_name(
85        camera_config, 'cameramodel')
86    if OK < gp.GP_OK:
87        OK, camera_model = gp.gp_widget_get_child_by_name(
88            camera_config, 'model')
89    if OK >= gp.GP_OK:
90        camera_model = camera_model.get_value()
91    else:
92        camera_model = ''
93    return camera_model
94
95def get_gphoto2_CameraWidgetType_string(innumenum):
96    switcher = {
97        0: "GP_WIDGET_WINDOW",
98        1: "GP_WIDGET_SECTION",
99        2: "GP_WIDGET_TEXT",
100        3: "GP_WIDGET_RANGE",
101        4: "GP_WIDGET_TOGGLE",
102        5: "GP_WIDGET_RADIO",
103        6: "GP_WIDGET_MENU",
104        7: "GP_WIDGET_BUTTON",
105        8: "GP_WIDGET_DATE"
106    }
107    return switcher.get(innumenum, "Invalid camwidget type")
108
109class PropCount(object):
110    def __init__(self):
111        self.numro = 0
112        self.numrw = 0
113        self.numtot = 0
114        self.numexc = 0
115    def __str__(self):
116        return "{},{},{},{}".format(self.numtot,self.numrw,self.numro,self.numexc)
117
118def get_formatted_ts(inunixts=None):
119    if inunixts is None:
120        unixts = time.time()
121    else:
122        unixts = inunixts
123    unixts = round(unixts,6)
124    tzlocalts = tzlocal.get_localzone().localize(datetime.utcfromtimestamp(unixts), is_dst=None).replace(microsecond=0)
125    isots = tzlocalts.isoformat(' ')
126    fsuffts = tzlocalts.strftime('%Y%m%d_%H%M%S') # file suffix timestamp
127    return (unixts, isots, fsuffts)
128
129def get_stamped_filename(infilename, inunixts):
130    unixts, isots, fsuffts = get_formatted_ts( inunixts )
131    outfilename = re.sub(r"_\d{8}_\d{6}", "", infilename) # remove any of our timestamps
132    # "split" at first period (if any), and insert (or append) our timestamp there:
133    outfilename = re.sub(r'^([^.\n]*)([.]*)(.*)$', r'\1_'+fsuffts+r'\2\3', outfilename)
134    return outfilename
135
136def get_camera_config_children(childrenarr, savearr, propcount):
137    for child in childrenarr:
138        tmpdict = OrderedDict()
139        propcount.numtot += 1
140        if child.get_readonly():
141            propcount.numro += 1
142        else:
143            propcount.numrw += 1
144        tmpdict['idx'] = str(propcount)
145        tmpdict['ro'] = child.get_readonly()
146        tmpdict['name'] = child.get_name()
147        tmpdict['label'] = child.get_label()
148        tmpdict['type'] = child.get_type()
149        tmpdict['typestr'] = get_gphoto2_CameraWidgetType_string( tmpdict['type'] )
150        if ((tmpdict['type'] == gp.GP_WIDGET_RADIO) or (tmpdict['type'] == gp.GP_WIDGET_MENU)):
151            tmpdict['count_choices'] = child.count_choices()
152            tmpchoices = []
153            for choice in child.get_choices():
154                tmpchoices.append(choice)
155            tmpdict['choices'] = ",".join(tmpchoices)
156        if (child.count_children() > 0):
157            tmpdict['children'] = []
158            get_camera_config_children(child.get_children(), tmpdict['children'], propcount)
159        else:
160            # NOTE: camera HAS to be "into preview mode to raise mirror", otherwise at this point can get "gphoto2.GPhoto2Error: [-2] Bad parameters" for get_value
161            try:
162                tmpdict['value'] = child.get_value()
163            except Exception as ex:
164                tmpdict['value'] = "{} {}".format( type(ex).__name__, ex.args)
165                propcount.numexc += 1
166        savearr.append(tmpdict)
167
168def get_camera_config_object(camera_config, inunixts=None):
169    retdict = OrderedDict()
170    retdict['camera_model'] = get_camera_model(camera_config)
171    if inunixts is None: myunixts = time.time()
172    else: myunixts = inunixts
173    unixts, isots, fsuffts = get_formatted_ts( myunixts )
174    retdict['ts_taken_on'] = unixts
175    retdict['date_taken_on'] = isots
176    propcount = PropCount()
177    retarray = []
178    retdict['children'] = []
179    get_camera_config_children(camera_config.get_children(), retdict['children'], propcount)
180    excstr = "no errors - OK." if (propcount.numexc == 0) else "{} errors - bad (please check if camera mirror is up)!".format(propcount.numexc)
181    print("Parsed camera config: {} properties total, of which {} read-write and {} read-only; with {}".format(propcount.numtot, propcount.numrw, propcount.numro, excstr))
182    return retdict
183
184def put_camera_capture_preview_mirror(camera, camera_config, camera_model):
185    if camera_model == 'unknown':
186        # find the capture size class config item
187        # need to set this on my Canon 350d to get preview to work at all
188        OK, capture_size_class = gp.gp_widget_get_child_by_name(
189            camera_config, 'capturesizeclass')
190        if OK >= gp.GP_OK:
191            # set value
192            value = capture_size_class.get_choice(2)
193            capture_size_class.set_value(value)
194            # set config
195            camera.set_config(camera_config)
196    else:
197        # put camera into preview mode to raise mirror
198        ret = gp.gp_camera_capture_preview(camera) # OK, camera_file
199        #print(ret) # [0, <Swig Object of type 'CameraFile *' at 0x7fb5a0044a40>]
200
201def start_capture_view():
202    camera = gp.Camera()
203    hasCamInited = False
204    try:
205        camera.init() # prints: WARNING: gphoto2: (b'gp_context_error') b'Could not detect any camera' if logging set up
206        hasCamInited = True
207    except Exception as ex:
208        lastException = ex
209        print("No camera: {} {}; ".format( type(lastException).__name__, lastException.args))
210    if hasCamInited:
211        camera_config = camera.get_config()
212        camera_model = get_camera_model(camera_config)
213        put_camera_capture_preview_mirror(camera, camera_config, camera_model)
214        print("Started capture view (extended lens/raised mirror) on camera: {}".format(camera_model))
215        sys.exit(0)
216    else: # camera not inited
217        print("Sorry, no camera present, cannot execute command; exiting.")
218        sys.exit(1)
219
220def stop_capture_view():
221    camera = gp.Camera()
222    hasCamInited = False
223    try:
224        camera.init()
225        hasCamInited = True
226    except Exception as ex:
227        lastException = ex
228        print("No camera: {} {}; ".format( type(lastException).__name__, lastException.args))
229    if hasCamInited:
230        camera_config = camera.get_config()
231        camera_model = get_camera_model(camera_config)
232        # https://github.com/gphoto/gphoto2/issues/195
233        OK, capture = gp.gp_widget_get_child_by_name( camera_config, 'capture' )
234        if OK >= gp.GP_OK:
235            capture.set_value(0)
236            camera.set_config(camera_config)
237        print("Stopped capture view (retracted lens/released mirror) on camera: {} ({} {})".format(camera_model, OK, capture))
238        sys.exit(0)
239    else: # camera not inited
240        print("Sorry, no camera present, cannot execute command; exiting.")
241        sys.exit(1)
242
243# mostly from time_lapse.py (_send_file is from focus-gui.py)
244def do_capture_image(camera):
245    # adjust camera configuratiuon
246    cfg = camera.get_config()
247    capturetarget_cfg = cfg.get_child_by_name('capturetarget')
248    capturetarget = capturetarget_cfg.get_value()
249    capturetarget_cfg.set_value('Internal RAM')
250    camera.set_config(cfg)
251    # do capture
252    path = camera.capture(gp.GP_CAPTURE_IMAGE)
253    print('capture cam path: {} {}'.format(path.folder, path.name))
254    camera_file = camera.file_get(
255        path.folder, path.name, gp.GP_FILE_TYPE_NORMAL)
256    # saving of image implied in current directory:
257    camera_file.save(path.name)
258    camera.file_delete(path.folder, path.name)
259    # reset configuration
260    capturetarget_cfg.set_value(capturetarget)
261    camera.set_config(cfg)
262    return path.name
263
264
265def get_json_filters(args):
266    jsonfilters = []
267    if hasattr(args, 'include_names_json'):
268        splitnames = args.include_names_json.split(",")
269        for iname in splitnames:
270            splitnameeq = iname.split("=") # 1-element array if = not present; 2-element array if present
271            splitnameeq = list(filter(None, splitnameeq))
272            jsonfilters.append(splitnameeq)
273    jsonfilters = list(filter(None, jsonfilters))
274    return jsonfilters
275
276def copy_json_filter_props(inarr, outarr, inpropcount, outpropcount, jsonfilters):
277    for inchild in inarr:
278        inpropcount.numtot += 1
279        if inchild['ro'] == 1:
280            inpropcount.numro += 1
281        else:
282            inpropcount.numrw += 1
283        shouldCopy = False
284        if len(jsonfilters)>0:
285            for jfilter in jsonfilters:
286                if len(jfilter) == 2:
287                    key = jfilter[0] ; val = jfilter[1]
288                    if ( str(inchild[key]) == val ):
289                        shouldCopy = True
290                elif len(jfilter) == 1:
291                    if inchild["name"] == jfilter[0]:
292                        shouldCopy = True
293        else: # len(jsonfilters) == 0: ; no filters, pass everything
294            shouldCopy = True
295        # copy with flatten hierarchy - only copy dicts that have 'value' (else they have 'children'):
296        if ( ('value' in inchild.keys()) and (inchild['value'] is not None) ):
297            if shouldCopy:
298                outpropcount.numtot += 1
299                if inchild['ro'] == 1:
300                    outpropcount.numro += 1
301                else:
302                    outpropcount.numrw += 1
303                if 'Error' in str(inchild['value']):
304                    outpropcount.numexc += 1
305                outarr.append(inchild)
306        else: # if the child dict has no 'value' then it has 'children'
307            if 'children' in inchild.keys():
308                copy_json_filter_props(inchild['children'], outarr, inpropcount, outpropcount, jsonfilters)
309
310
311def do_GetSaveCamConfJson(camera, jsonfile, inunixts=None):
312    camera_config = camera.get_config() # may print: WARNING: gphoto2: (b'_get_config [config.c:7649]') b"Type of property 'Owner Name' expected: 0x4002 got: 0x0000"
313    camconfobj = get_camera_config_object(camera_config, inunixts)
314    with open(jsonfile, 'wb') as f:
315        json.dump(camconfobj, codecs.getwriter('utf-8')(f), ensure_ascii=False, indent=2, separators=(',', ': '))
316    print("Saved config to {}".format(jsonfile))
317
318
319def getSaveCamConfJson(args):
320    if (not(args.save_cam_conf_json)):
321        print("getSaveCamConfJson: Sorry, unusable/empty output .json filename; aborting")
322        sys.exit(1)
323    jsonfile = os.path.realpath(args.save_cam_conf_json)
324    print("getSaveCamConfJson: saving to {}".format(jsonfile))
325    camera = gp.Camera()
326    hasCamInited = False
327    try:
328        camera.init()
329        hasCamInited = True
330    except Exception as ex:
331        lastException = ex
332        print("No camera: {} {}; ".format( type(lastException).__name__, lastException.args))
333    if hasCamInited:
334        do_GetSaveCamConfJson(camera, jsonfile)
335        print("Exiting.")
336        sys.exit(0)
337    else: # camera not inited
338        print("Sorry, no camera present, cannot execute command; exiting.")
339        sys.exit(1)
340
341def copyFilterCamConfJson(args):
342    if (not(args.load_cam_conf_json) or not(args.save_cam_conf_json)):
343        print("copyFilterCamConfJson: Sorry, unusable/empty input or output .json filename; aborting")
344        sys.exit(1)
345    injsonfile = os.path.realpath(args.load_cam_conf_json)
346    outjsonfile = os.path.realpath(args.save_cam_conf_json)
347    print("loadSetCamConfJson: input load from {}, output save to {}".format(injsonfile, outjsonfile))
348    # no need for camera here
349    # open and parse injsonfile as object
350    with open(injsonfile) as in_data_file:
351        indatadict = json.load(in_data_file, object_pairs_hook=OrderedDict)
352    # check filters
353    jsonfilters = get_json_filters(args)
354    jsonfilterslen = len(jsonfilters)
355    if jsonfilterslen > 0:
356        print("Got {} json filters; including only properties names(/values) in filters in output file".format(jsonfilterslen))
357    else:
358        print("Got no ({}) json filters; copying input (flattened) to output file".format(jsonfilterslen))
359    retdict = OrderedDict()
360    retdict['camera_model'] = indatadict['camera_model']
361    retdict['orig_ts_taken_on'] = indatadict['ts_taken_on']
362    retdict['orig_date_taken_on'] = indatadict['date_taken_on']
363    unixts, isots, fsuffts = get_formatted_ts( time.time() )
364    retdict['ts_taken_on'] = unixts
365    retdict['date_taken_on'] = isots
366    inpropcount = PropCount()
367    outpropcount = PropCount()
368    retdict['children'] = []
369    copy_json_filter_props(indatadict['children'], retdict['children'], inpropcount, outpropcount, jsonfilters)
370    print("Processed input props: {} ro, {} rw, {} total; got output props: {} ro, {} rw, {} total".format(
371        inpropcount.numro, inpropcount.numrw, inpropcount.numtot, outpropcount.numro, outpropcount.numrw, outpropcount.numtot
372    ))
373    # save
374    with open(outjsonfile, 'wb') as f:
375        json.dump(retdict, codecs.getwriter('utf-8')(f), ensure_ascii=False, indent=2, separators=(',', ': '))
376    print("Saved filtered copy to output file {}".format(outjsonfile))
377    sys.exit(0)
378
379def do_LoadSetCamConfJson(camera, injsonfile):
380    camera_config = camera.get_config()
381    # open and parse injsonfile as object
382    with open(injsonfile) as in_data_file:
383        indatadict = json.load(in_data_file, object_pairs_hook=OrderedDict)
384    print("Getting flattened object (removes hierarchy/nodes with only children and without value) from {} ...".format(injsonfile))
385    inpropcount = PropCount()
386    outpropcount = PropCount()
387    jsonfilters = [] # empty here
388    flatproparray = []
389    copy_json_filter_props(indatadict['children'], flatproparray, inpropcount, outpropcount, jsonfilters)
390    print("Flattened {} ro, {} rw, {} total input props to: {} ro, {} rw, {} total flat props".format(
391        inpropcount.numro, inpropcount.numrw, inpropcount.numtot, outpropcount.numro, outpropcount.numrw, outpropcount.numtot
392    ))
393    print("Applying at most {} read/write props (ignoring {} read-only ones, out of {} total props) to camera:".format(
394        outpropcount.numrw, outpropcount.numro, len(flatproparray)
395    ))
396    numappliedprops = 0
397    usedlabels = [] ; usednames = []
398    # NOTE - some of the camera properties depend on others;
399    # say for shootingmode=Manual, some vars are r/w, but for shootingmode=Auto, same vars become r/o
400    # thus we split off flatproparray in two parts, executed first the one, then the other
401    flatproparrayfirst = []
402    for iname in PROPNAMESTOSETFIRST: # SO: 54343917
403        # find the object having the name iname
404        foundidx = -1
405        for ix, idict in enumerate(flatproparray):
406            if idict.get('name') == iname:
407                foundidx = ix
408                break
409        if foundidx > -1:
410            # remove dict object via pop at index, save removed object
411            remdict = flatproparray.pop(foundidx)
412            # add removed object to newarr:
413            flatproparrayfirst.append(remdict)
414    print("First pass (of applying cam settings):")
415    usednamesfirst = []
416    passedpropsfirst = 0
417    # no need to check duplicates or blacklisted here:
418    for ix, tprop in enumerate(flatproparrayfirst):
419        if tprop['ro'] == 1:
420            print(" {:3d}: (ignoring read-only prop '{}' ({}))".format(ix+1, tprop['name'], tprop['label'] ))
421        else:
422            numappliedprops += 1
423            print(" {:3d}: Applying prop {}/{}: '{}' = '{}' ({}))".format(ix+1, numappliedprops, outpropcount.numrw, tprop['name'], tprop['value'], tprop['label']))
424            usedlabels.append(tprop['label'])
425            usednamesfirst.append(tprop['name'])
426            OK, gpprop = gp.gp_widget_get_child_by_name( camera_config, "{}".format(tprop['name']) )
427            if OK >= gp.GP_OK:
428                if ( type(tprop['value']).__name__ == "unicode" ):
429                    gpprop.set_value( "{}".format(tprop['value']) )
430                else:
431                    gpprop.set_value( tprop['value'] )
432        passedpropsfirst = ix+1
433    print("  applying props: {}.".format(",".join(usednamesfirst)))
434    camera.set_config(camera_config)
435    # sleeping for 5 sec seems enough to allow changes between auto and manual shootingmode, without taking a picture... but 1 sec is not; 2 seems enough..
436    time.sleep(SLEEPWAITCHANGE)
437    print("Second pass (of applying cam settings):")
438    for ix, tprop in enumerate(flatproparray):
439        propnum = passedpropsfirst + ix + 1
440        if tprop['ro'] == 1:
441            print(" {:3d}: (ignoring read-only prop '{}' ({}))".format(propnum, tprop['name'], tprop['label'] ))
442        # only look for exact duplicates - rest, if needed, can be blacklisted:
443        elif ( tprop['label'] in usedlabels ):
444            print(" {:3d}: (ignoring duplicate label prop '{}' ({}))".format(propnum, tprop['name'], tprop['label'] ))
445        elif ( any([pat.match(tprop['name']) for pat in BLACKLISTPROPSREGEX]) ):
446            print(" {:3d}: (ignoring blacklisted name prop '{}' ({}))".format(propnum, tprop['name'], tprop['label'] ))
447        else:
448            numappliedprops += 1
449            print(" {:3d}: Applying prop {}/{}: '{}' = '{}' ({}))".format(propnum, numappliedprops, outpropcount.numrw, tprop['name'], tprop['value'], tprop['label']))
450            usedlabels.append(tprop['label'])
451            usednames.append(tprop['name'])
452            OK, gpprop = gp.gp_widget_get_child_by_name( camera_config, "{}".format(tprop['name']) )
453            if OK >= gp.GP_OK:
454                if ( type(tprop['value']).__name__ == "unicode" ):
455                    gpprop.set_value( "{}".format(tprop['value']) )
456                else:
457                    gpprop.set_value( tprop['value'] )
458    print("  applying props: {} (+ {}).".format( ",".join(usednames), ",".join(usednamesfirst) ))
459    camera.set_config(camera_config)
460    print("Applied {} properties from file {} to camera.".format(numappliedprops, injsonfile))
461
462def loadSetCamConfJson(args):
463    if (not(args.load_cam_conf_json)):
464        print("loadSetCamConfJson: Sorry, unusable/empty output .json filename; aborting")
465        sys.exit(1)
466    injsonfile = os.path.realpath(args.load_cam_conf_json)
467    print("loadSetCamConfJson: loading from {}".format(injsonfile))
468    camera = gp.Camera()
469    ctx = gp.Context()
470    hasCamInited = False
471    try:
472        camera.init(ctx)
473        hasCamInited = True
474    except Exception as ex:
475        lastException = ex
476        print("No camera: {} {}; ".format( type(lastException).__name__, lastException.args))
477    if hasCamInited:
478        do_LoadSetCamConfJson(camera, injsonfile)
479        print("Exiting.")
480        sys.exit(0)
481    else:
482        print("Sorry, no camera present, cannot execute command; exiting.")
483        sys.exit(1)
484
485
486# SO:35514531 - see also SO:46934526, 40683840, 9475772, https://github.com/baoboa/pyqt5/blob/master/examples/widgets/imageviewer.py
487class PhotoViewer(QtWidgets.QGraphicsView):
488    photoClicked = QtCore.pyqtSignal(QtCore.QPoint)
489
490    def __init__(self, parent):
491        super(PhotoViewer, self).__init__(parent)
492        self.parent = parent
493        self.isMouseOver = False
494        self.ZOOMFACT = 1.25
495        self._zoom = 0
496        self._zoomfactor = 1
497        self._empty = True
498        self._scene = QtWidgets.QGraphicsScene(self)
499        self._photo = QtWidgets.QGraphicsPixmapItem()
500        self._scene.addItem(self._photo)
501        self.setScene(self._scene)
502        self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
503        self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
504        self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
505        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
506        self.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(30, 30, 30)))
507        self.setFrameShape(QtWidgets.QFrame.NoFrame)
508
509    def hasPhoto(self):
510        return not self._empty
511
512    def printUnityFactor(self):
513        rect = QtCore.QRectF(self._photo.pixmap().rect())
514        unity = self.transform().mapRect(QtCore.QRectF(0, 0, 1, 1))
515        viewrect = self.viewport().rect()
516        scenerect = self.transform().mapRect(rect)
517        factor = min(viewrect.width() / scenerect.width(),
518                     viewrect.height() / scenerect.height())
519        print("puf factor {} vr_w {} sr_w {} u_w {} vr_h {} sr_h {} u_h {} ".format(factor, viewrect.width(), scenerect.width(), unity.width(), viewrect.height(), scenerect.height(), unity.height() ))
520
521    def fitInView(self, scale=True):
522        rect = QtCore.QRectF(self._photo.pixmap().rect())
523        if not rect.isNull():
524            self.setSceneRect(rect)
525            if self.hasPhoto():
526                unity = self.transform().mapRect(QtCore.QRectF(0, 0, 1, 1))
527                self.scale(1 / unity.width(), 1 / unity.height())
528                viewrect = self.viewport().rect()
529                scenerect = self.transform().mapRect(rect)
530                factor = min(viewrect.width() / scenerect.width(),
531                             viewrect.height() / scenerect.height())
532                # here, view scaled to fit:
533                self._zoomfactor = factor
534                self._zoom = math.log( self._zoomfactor, self.ZOOMFACT )
535                self.scale(factor, factor)
536                self.parent.updateStatusBar()
537                if (self.isMouseOver): # should be true on wheel, regardless
538                    self.setDragState()
539
540    def setPhoto(self, pixmap=None):
541        if pixmap and not pixmap.isNull():
542            self._empty = False
543            self._photo.setPixmap(pixmap)
544        else:
545            self._empty = True
546            self._photo.setPixmap(QtGui.QPixmap())
547
548    def resetZoom(self):
549        if self.hasPhoto():
550            self._zoom = 0
551            self._zoomfactor = 1.0
552            unity = self.transform().mapRect(QtCore.QRectF(0, 0, 1, 1))
553            self.scale(1 / unity.width(), 1 / unity.height())
554            self.parent.updateStatusBar()
555            if (self.isMouseOver):
556                self.setDragState()
557
558    def zoomPlus(self):
559        if self.hasPhoto():
560            factor = self.ZOOMFACT # 1.25
561            self._zoomfactor = self._zoomfactor * self.ZOOMFACT
562            self._zoom += 1
563            self.scale(factor, factor)
564            self.parent.updateStatusBar()
565            self.setDragState()
566
567    def zoomMinus(self):
568        if self.hasPhoto():
569            factor = 1.0/self.ZOOMFACT #0.8
570            self._zoomfactor = self._zoomfactor / self.ZOOMFACT
571            self._zoom -= 1
572            self.scale(factor, factor)
573            self.parent.updateStatusBar()
574            self.setDragState()
575
576    def wheelEvent(self, event):
577        if self.hasPhoto():
578            if event.angleDelta().y() > 0:
579                self.zoomPlus()
580            else:
581                self.zoomMinus()
582
583    def mousePressEvent(self, event):
584        if self._photo.isUnderMouse():
585            self.photoClicked.emit(QtCore.QPoint(event.pos()))
586        super(PhotoViewer, self).mousePressEvent(event)
587
588    def getCanDrag(self):
589        return ((self.horizontalScrollBar().maximum() > 0) or (self.verticalScrollBar().maximum() > 0))
590
591    def setDragState(self):
592        # here we mostly want to take case of the mouse cursor/pointer - and show the hand only when dragging is possible
593        canDrag = self.getCanDrag()
594        if (canDrag):
595            self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
596        else:
597            self.setDragMode(QtWidgets.QGraphicsView.NoDrag)
598
599    def enterEvent(self, event):
600        self.isMouseOver = True
601        self.setDragState()
602        return super(PhotoViewer, self).enterEvent(event)
603
604    def leaveEvent(self, event):
605        self.isMouseOver = False
606        # no need for setDragState - is autohandled, as we leave
607        return super(PhotoViewer, self).enterEvent(event)
608
609# monkeypatch ccgoo.RangeWidget - it reacted only on self.sliderReleased.connect(self.new_value),
610# which reacts only if slider dragged first, making it impossible to control from say VNC
611class MyRangeWidget(QtWidgets.QSlider):
612    def __init__(self, config_changed, config, parent=None):
613        QtWidgets.QSlider.__init__(self, Qt.Horizontal, parent)
614        self.config_changed = config_changed
615        self.config = config
616        if self.config.get_readonly():
617            self.setDisabled(True)
618        assert self.config.count_children() == 0
619        lo, hi, self.inc = self.config.get_range()
620        value = self.config.get_value()
621        self.setRange(int(lo * self.inc), int(hi * self.inc))
622        self.setValue(int(value * self.inc))
623        self.valueChanged.connect(self.new_value)
624
625    def mousePressEvent(self, e):
626        if e.button() == Qt.LeftButton:
627            e.accept()
628            x = e.pos().x()
629            value = (self.maximum() - self.minimum()) * x / self.width() + self.minimum()
630            self.setValue(value)
631        else:
632            return super().mousePressEvent(self, e)
633
634    def new_value(self):
635        value = float(self.value()) * self.inc
636        self.config.set_value(value)
637        self.config_changed()
638ccgoo.RangeWidget = MyRangeWidget
639
640class MainWindow(QtWidgets.QMainWindow):
641    quit_action = None
642    new_image_sig = QtCore.pyqtSignal(object)
643
644    def _reset_config(self):
645        if self.hasCamInited:
646            if self.old_capturetarget is not None:
647                # find the capture target item
648                OK, capture_target = gp.gp_widget_get_child_by_name(
649                    self.camera_config, 'capturetarget')
650                if OK >= gp.GP_OK:
651                    # set config
652                    capture_target.set_value(self.old_capturetarget)
653                    self.camera.set_config(self.camera_config)
654                    self.old_capturetarget = None
655
656    def closeEvent(self, event):
657        if self.hasCamInited:
658            self.running = False
659            self._reset_config()
660            self.camera.exit()
661        self.settings.setValue("geometry", self.saveGeometry())
662        self.settings.setValue("windowState", self.saveState())
663        return super(MainWindow, self).closeEvent(event)
664
665    def _set_config(self):
666        if self.hasCamInited:
667            # find the capture target item
668            OK, capture_target = gp.gp_widget_get_child_by_name(
669                self.camera_config, 'capturetarget')
670            if OK >= gp.GP_OK:
671                if self.old_capturetarget is None:
672                    self.old_capturetarget = capture_target.get_value()
673                choice_count = capture_target.count_choices()
674                for n in range(choice_count):
675                    choice = capture_target.get_choice(n)
676                    if 'internal' in choice.lower():
677                        # set config
678                        capture_target.set_value(choice)
679                        self.camera.set_config(self.camera_config)
680                        break
681            # find the image format config item
682            # camera dependent - 'imageformat' is 'imagequality' on some
683            OK, image_format = gp.gp_widget_get_child_by_name(
684                self.camera_config, 'imageformat')
685            if OK >= gp.GP_OK:
686                # get current setting
687                value = image_format.get_value()
688                # make sure it's not raw
689                if 'raw' in value.lower():
690                    print('Cannot preview raw images')
691                    return False
692            return True
693        else:
694            return False
695
696    def set_splitter(self, inqtsplittertype, inwidget1, inwidget2):
697        if (hasattr(self,'splitter1')):
698            self.mainwid.layout().removeWidget(self.splitter1)
699            self.splitter1.close()
700        self.splitter1 = QtWidgets.QSplitter(inqtsplittertype)
701        self.splitter1.addWidget(inwidget1)
702        self.splitter1.addWidget(inwidget2)
703        self.splitter1.setSizes([600, 600]); # equal splitter at start
704        self.mainwid.layout().addWidget(self.splitter1)
705        self.mainwid.layout().update()
706
707    def create_main_menu(self):
708        self.mainMenu = self.menuBar()
709        self.fileMenu = self.mainMenu.addMenu('&File')
710        # actions
711        self.load_action = QtWidgets.QAction('&Load settings', self)
712        self.load_action.setShortcuts(['Ctrl+L'])
713        self.load_action.triggered.connect(self.load_settings)
714        self.save_action = QtWidgets.QAction('&Save settings', self)
715        self.save_action.setShortcuts(['Ctrl+S'])
716        self.save_action.triggered.connect(self.save_settings)
717        self.fileMenu.addAction(self.load_action)
718        self.fileMenu.addAction(self.save_action)
719        self.fileMenu.addAction(self.quit_action)
720        self.viewMenu = self.mainMenu.addMenu('&View')
721        self.zoomorig_action = QtWidgets.QAction('&Zoom original', self)
722        self.zoomorig_action.setShortcuts(['Ctrl+Z'])
723        self.zoomorig_action.triggered.connect(self.zoom_original)
724        self.zoomfitview_action = QtWidgets.QAction('Zoom to &fit view', self)
725        self.zoomfitview_action.setShortcuts(['Ctrl+F'])
726        self.zoomfitview_action.triggered.connect(self.zoom_fit_view)
727        self.zoomplus_action = QtWidgets.QAction('Zoom plu&s', self)
728        self.zoomplus_action.setShortcuts(['+'])
729        self.zoomplus_action.triggered.connect(self.zoom_plus)
730        self.zoomminus_action = QtWidgets.QAction('Zoom &minus', self)
731        self.zoomminus_action.setShortcuts(['-'])
732        self.zoomminus_action.triggered.connect(self.zoom_minus)
733        self.switchlayout_action = QtWidgets.QAction('Switch &Layout', self)
734        self.switchlayout_action.setShortcuts(['Ctrl+A'])
735        self.switchlayout_action.triggered.connect(self.switch_splitter_layout)
736        self.dopreview_action = QtWidgets.QAction('Do &Preview', self)
737        self.dopreview_action.setShortcuts(['Ctrl+X'])
738        self.dopreview_action.triggered.connect(self._do_preview)
739        self.repeatpreview_action = QtWidgets.QAction('&Repeat Preview', self)
740        self.repeatpreview_action.setShortcuts(['Ctrl+R'])
741        self.repeatpreview_action.setCheckable(True)
742        self.repeatpreview_action.setChecked(False)
743        self.repeatpreview_action.triggered.connect(self.continuous)
744        self.docapture_action = QtWidgets.QAction('Capture &Image', self)
745        self.docapture_action.setShortcuts(['Ctrl+I'])
746        self.docapture_action.triggered.connect(self._capture_image)
747        self.viewMenu.addAction(self.switchlayout_action)
748        self.viewMenu.addAction(self.dopreview_action)
749        self.viewMenu.addAction(self.repeatpreview_action)
750        self.viewMenu.addAction(self.docapture_action)
751        self.viewMenu.addAction(self.zoomorig_action)
752        self.viewMenu.addAction(self.zoomfitview_action)
753        self.viewMenu.addAction(self.zoomplus_action)
754        self.viewMenu.addAction(self.zoomminus_action)
755
756    def replicate_ccgoo_main_window(self):
757        # main widget
758        self.widget = QtWidgets.QWidget()
759        self.widget.setLayout(QtWidgets.QGridLayout())
760        self.widget.layout().setColumnStretch(0, 1)
761        # 'apply' button
762        if self.args.config_apply_btn == 1:
763            self.apply_button = QtWidgets.QPushButton('apply changes')
764            self.apply_button.setEnabled(False)
765            self.apply_button.clicked.connect(self.apply_changes)
766            self.widget.layout().addWidget(self.apply_button, 1, 1)
767
768    def eventFilter(self, source, event):
769        return super(MainWindow, self).eventFilter(source, event)
770
771    def replicate_fg_viewer(self):
772        self.image_display = PhotoViewer(self)
773        self.frameviewlayout.addWidget(self.image_display)
774        self.new_image_sig.connect(self.new_image)
775
776    def updateStatusBar(self):
777        msgstr = ""
778        if self.lastException:
779            msgstr = "No camera: {} {}; ".format( type(self.lastException).__name__, self.lastException.args)
780        if self.hasCamInited:
781            msgstr = "Camera model: {} ; ".format(self.camera_model if (self.camera_model) else "No info")
782            if self.lastImageSize:
783                msgstr += "last imgsize: {} x {} ".format(self.lastImageSize[0], self.lastImageSize[1])
784        zoomstr = "zoom: {:.3f} {:.3f}".format(self.image_display._zoom, self.image_display._zoomfactor)
785        def replacer(m):
786            retstr = m.group(1).replace(r'0', ' ')+' '*len(m.group(2))
787            return retstr
788        zoomstr = patTrailDigSpace.sub(lambda m: replacer(m), zoomstr)
789        msgstr += zoomstr
790        if self.fps:
791            msgstr += " ; {}".format(self.fps)
792            self.fps = ""
793        if self.singleStatusMsg:
794            msgstr += " ({})".format(self.singleStatusMsg)
795            self.singleStatusMsg = ""
796        self.statusBar().showMessage(msgstr)
797
798    def checkCreateNoCamImg(self):
799        nocamimgpath = os.path.join(THISSCRIPTDIR, NOCAMIMG)
800        if (not(os.path.isfile(nocamimgpath))):
801            # create gradient background with text:
802            nocamsize = (320, 240)
803            nocamsizec = (nocamsize[0]/2, nocamsize[1]/2)
804            colA = (80, 80, 80) ; colB = (210, 30, 30) ; # colB is center
805            nocamimg = Image.new('RGB', nocamsize, color=0xFF)
806            gradoffset = (-50, 20)
807            nocammaxd = math.sqrt((nocamsizec[0]+abs(gradoffset[0]))**2 + (nocamsizec[1]+abs(gradoffset[1]))**2)
808            for ix in range(nocamsize[0]):
809                for iy in range(nocamsize[1]):
810                    dist = math.sqrt( (ix-nocamsizec[0]-gradoffset[0])**2 + (iy-nocamsizec[1]-gradoffset[1])**2 )
811                    distnorm = dist/nocammaxd
812                    r = colA[0]*distnorm + colB[0]*(1.0-distnorm)
813                    g = colA[1]*distnorm + colB[1]*(1.0-distnorm)
814                    b = colA[2]*distnorm + colB[2]*(1.0-distnorm)
815                    nocamimg.putpixel((ix, iy), (int(r), int(g), int(b)) )
816            d = ImageDraw.Draw(nocamimg)
817            d.text((20,nocamsizec[1]-4), "No camera (unknown)", fill=(240,240,240))
818            nocamimg.save(nocamimgpath)
819
820
821    def __init__(self, args):
822        self.args = args
823        self.current_splitter_style=0
824        self.lastImageSize = None
825        self.lastImageType = None # 0 - preview; 1 - capture image
826        self.timestamp = None
827        self.fps = ""
828        self.lastException = None
829        self.lastOpenPath = None
830        self.lastSavePath = None
831        self.singleStatusMsg = ""
832        self.hasCamInited = False
833        self.do_init = QtCore.QEvent.registerEventType()
834        self.do_next = QtCore.QEvent.registerEventType()
835        self.running = False
836        QtWidgets.QMainWindow.__init__(self)
837        self.settings = QtCore.QSettings("MyCompany", APPNAME)
838        if not self.settings.value("geometry") == None:
839            self.restoreGeometry(self.settings.value("geometry"))
840        if not self.settings.value("windowState") == None:
841            self.restoreState(self.settings.value("windowState"))
842        self.setWindowTitle("Camera config {}".format(APPNAME))
843        self.setMinimumWidth(1000)
844        self.setMinimumHeight(600)
845        # quit shortcut
846        self.quit_action = QtWidgets.QAction('&Quit', self)
847        self.quit_action.setShortcuts(['Ctrl+Q', 'Ctrl+W'])
848        self.quit_action.setStatusTip('Exit application')
849        self.quit_action.triggered.connect(QtWidgets.qApp.closeAllWindows)
850        self.addAction(self.quit_action)
851        # main menu
852        self.create_main_menu()
853        # replicate main window from camera-config-gui-oo
854        self.replicate_ccgoo_main_window()
855        # frames
856        self.frameview = QtWidgets.QFrame(self)
857        self.frameview.setFrameShape(QtWidgets.QFrame.StyledPanel)
858        self.frameviewlayout = QtWidgets.QGridLayout(self.frameview)
859        self.frameviewlayout.setSpacing(0);
860        self.frameviewlayout.setContentsMargins(0,0,0,0);
861
862        self.checkCreateNoCamImg()
863
864        self.replicate_fg_viewer()
865
866        self.frameconf = QtWidgets.QFrame(self)
867        self.frameconf.setFrameShape(QtWidgets.QFrame.StyledPanel)
868        self.frameconf.setStyleSheet("padding: 0;") # nope
869        self.frameconflayout = QtWidgets.QHBoxLayout(self.frameconf)
870        self.frameconflayout.setSpacing(0);
871        self.frameconflayout.setContentsMargins(0,0,0,0);
872        self.scrollconf = QtWidgets.QScrollArea(self)
873        self.scrollconf.setWidgetResizable(False)
874        # self.contentconf is just used for init here; afterward is replaced by self.widget (self.configsection)
875        self.contentconf = QtWidgets.QWidget()
876        self.contentconf.setLayout(QtWidgets.QGridLayout())
877        self.contentconf.layout().setColumnStretch(0, 1)
878        self.scrollconf.setWidget(self.contentconf)
879        self.frameconflayout.addWidget(self.scrollconf)
880
881        self.mainwid = QtWidgets.QWidget()
882        self.mainwid.setLayout(QtWidgets.QGridLayout())
883        self.setCentralWidget(self.mainwid)
884        self.set_splitter(Qt.Horizontal, self.frameview, self.frameconf)
885
886        self.camera = gp.Camera()
887        self.ctx = gp.Context()
888        QtWidgets.QApplication.postEvent(
889            self, QtCore.QEvent(self.do_init), Qt.LowEventPriority - 1)
890
891    def event(self, event):
892        if ( (event.type() != self.do_init) and (event.type() != self.do_next) ):
893            return QtWidgets.QMainWindow.event(self, event)
894        event.accept()
895        if event.type() == self.do_init:
896            QtWidgets.QApplication.setOverrideCursor(Qt.WaitCursor)
897            try:
898                self.initialise()
899            finally:
900                QtWidgets.QApplication.restoreOverrideCursor()
901            return True
902        elif event.type() == self.do_next:
903            self._do_continuous()
904            return True
905
906    def _do_continuous(self):
907        if not self.running:
908            self._reset_config()
909            return
910        if self.hasCamInited:
911             self._do_preview()
912        else:
913            self._do_preview_camnotinit()
914        # post event to trigger next capture
915        QtWidgets.QApplication.postEvent(
916            self, QtCore.QEvent(self.do_next), Qt.LowEventPriority - 1)
917
918    @QtCore.pyqtSlot()
919    def continuous(self):
920        if self.running:
921            self.running = False
922            self.updateStatusBar() # clear last singleStatusMsg - clears fps upon stop
923            return
924        self.running = True
925        self._do_continuous()
926
927    def CameraHandlerInit(self):
928        # get camera config tree
929        self.hasCamInited = False
930        self.lastException = None
931        try:
932            self.camera.init(self.ctx)
933            self.hasCamInited = True
934        except Exception as ex:
935            self.lastException = ex
936            if type(ex) == gp.GPhoto2Error:
937                nocamimgpath = os.path.join(THISSCRIPTDIR, NOCAMIMG)
938                temppixmap = QtGui.QPixmap(nocamimgpath)
939                self.image_display.setPhoto(temppixmap)
940        if self.hasCamInited:
941            self.camera_config = self.camera.get_config()
942            # from CameraHandler::Init:
943            self.old_capturetarget = None
944            # get the camera model
945            self.camera_model = get_camera_model(self.camera_config)
946            put_camera_capture_preview_mirror(self.camera, self.camera_config, self.camera_model)
947        self.updateStatusBar()
948
949    def recreate_section_widget(self):
950        if ( hasattr(self, 'configsection') and (self.configsection) ):
951            self.widget.layout().removeWidget(self.configsection)
952        self.configsection = ccgoo.SectionWidget(self.config_changed, self.camera_config)
953        self.widget.layout().addWidget(self.configsection, 0, 0, 1, 3)
954        self.scrollconf.setWidget(self.widget)
955
956    def initialise(self):
957        self.CameraHandlerInit()
958        if self.hasCamInited:
959            # create corresponding tree of tab widgets
960            self.setWindowTitle(self.camera_config.get_label())
961            self.recreate_section_widget()
962
963    def config_changed(self):
964        if self.args.config_apply_btn == 1:
965            self.apply_button.setEnabled(True)
966        else:
967            def handler():
968                self.apply_changes()
969                timer.stop()
970                timer.deleteLater()
971            timer = QtCore.QTimer()
972            timer.timeout.connect(handler)
973            timer.start(0)
974
975    def reconstruct_config_section(self):
976        # assumes first setup already done, so there are existing tabs:
977        tabs = self.configsection.children()[1]
978        lastindex = tabs.currentIndex()
979        # get also the values of self.scrollconf scrollbars
980        lastconfhscroll = (self.scrollconf.horizontalScrollBar().value(), self.scrollconf.horizontalScrollBar().minimum(), self.scrollconf.horizontalScrollBar().singleStep(), self.scrollconf.horizontalScrollBar().pageStep(), self.scrollconf.horizontalScrollBar().maximum())
981        lastconfvscroll = (self.scrollconf.verticalScrollBar().value(), self.scrollconf.verticalScrollBar().minimum(), self.scrollconf.verticalScrollBar().singleStep(), self.scrollconf.verticalScrollBar().pageStep(), self.scrollconf.verticalScrollBar().maximum())
982        # Pre-reconstruct
983        self.camera_config = self.camera.get_config()
984        self.replicate_ccgoo_main_window()
985        self.recreate_section_widget()
986        tabs = self.configsection.children()[1]
987        tabs.setCurrentIndex(lastindex)
988        # Post-reconstruct
989        self.scrollconf.horizontalScrollBar().setValue(lastconfhscroll[0])
990        self.scrollconf.verticalScrollBar().setValue(lastconfvscroll[0])
991
992    def apply_changes(self):
993        self.camera.set_config(self.camera_config)
994        # here we'd need to reconstruct, to get the proper config values
995        self.reconstruct_config_section()
996
997    def load_settings(self):
998        self.updateStatusBar() # clear last singleStatusMsg
999        if self.lastOpenPath is not None:
1000            startpath, startfile = os.path.split(self.lastOpenPath)
1001        elif self.lastSavePath is not None:
1002            startpath, startfile = os.path.split(self.lastSavePath)
1003        else: # both None
1004            startpath = os.getcwd()
1005            startfile = ""
1006        options = QFileDialog.Options()
1007        options |= QFileDialog.DontUseNativeDialog
1008        fileName, _ = QFileDialog.getOpenFileName(self, "Load Camera Settings", os.path.join(startpath, startfile), "JSON Text Files (*.json);;All Files (*)", options=options)
1009        if fileName:
1010            do_LoadSetCamConfJson(self.camera, fileName)
1011            # update GUI also
1012            self.camera_config = self.camera.get_config()
1013            # avoid GUI context
1014            def handler():
1015                self.apply_changes()
1016                timer.stop()
1017                timer.deleteLater()
1018            timer = QtCore.QTimer()
1019            timer.timeout.connect(handler)
1020            timer.start(0)
1021            self.lastOpenPath = fileName
1022            self.singleStatusMsg = "loaded file to cam config; see terminal stdout for more"
1023            self.updateStatusBar()
1024
1025    def save_settings(self):
1026        self.updateStatusBar() # clear last singleStatusMsg
1027        if self.lastSavePath is not None:
1028            startpath, startfile = os.path.split(self.lastSavePath)
1029        else: # both None
1030            startpath = os.getcwd()
1031            startfile = "{}.json".format( re.sub(r'\s+', '', self.camera_model) )
1032        unixts = time.time()
1033        startfile = get_stamped_filename(startfile, unixts)
1034        options = QFileDialog.Options()
1035        options |= QFileDialog.DontUseNativeDialog
1036        fileName, _ = QFileDialog.getSaveFileName(self, "Save Camera Settings", os.path.join(startpath, startfile), "JSON Text Files (*.json);;All Files (*)", options=options)
1037        if fileName:
1038            do_GetSaveCamConfJson(self.camera, fileName, unixts)
1039            self.lastSavePath = fileName
1040            self.singleStatusMsg = "saved cam config to file; see terminal stdout for more"
1041            self.updateStatusBar()
1042
1043    def zoom_original(self):
1044        self.image_display.resetZoom()
1045
1046    def zoom_fit_view(self):
1047        self.image_display.fitInView()
1048
1049    def zoom_plus(self):
1050        self.image_display.zoomPlus()
1051
1052    def zoom_minus(self):
1053        self.image_display.zoomMinus()
1054
1055    def set_splitter_layout_style(self):
1056        if self.current_splitter_style == 0:
1057            self.set_splitter(Qt.Horizontal, self.frameview, self.frameconf)
1058        elif self.current_splitter_style == 1:
1059            self.set_splitter(Qt.Vertical, self.frameview, self.frameconf)
1060        elif self.current_splitter_style == 2:
1061            self.set_splitter(Qt.Horizontal, self.frameconf, self.frameview)
1062        elif self.current_splitter_style == 3:
1063            self.set_splitter(Qt.Vertical, self.frameconf, self.frameview)
1064
1065    def switch_splitter_layout(self):
1066        self.current_splitter_style = (self.current_splitter_style + 1) % 4
1067        self.set_splitter_layout_style()
1068
1069    def _do_preview(self):
1070        # capture preview image
1071        OK, camera_file = gp.gp_camera_capture_preview(self.camera)
1072        if OK < gp.GP_OK:
1073            print('Failed to capture preview')
1074            self.running = False
1075            return
1076        self._send_file(camera_file)
1077
1078    def _do_preview_camnotinit(self):
1079        # since cam not inited here, just load the cam-conf-no-cam.png/NOCAMIMG
1080        nocamimgpath = os.path.join(THISSCRIPTDIR, NOCAMIMG)
1081        image = Image.open(nocamimgpath)
1082        image.load()
1083        self.singleStatusMsg = "No cam img: {} x {}".format(image.size[0], image.size[1])
1084        self.new_image_sig.emit(image)
1085
1086    def _capture_image(self):
1087        startts = time.time()
1088        self.running = False
1089        self.repeatpreview_action.setChecked(False) # needed just for the menu item
1090        self.singleStatusMsg = "Capturing image - wait ..."
1091        self.updateStatusBar() # clear last singleStatusMsg
1092        imgfilename = do_capture_image(self.camera)
1093        imgpathname = os.path.realpath(imgfilename)
1094        image = Image.open(imgpathname)
1095        image.load()
1096        self.new_image_sig.emit(image)
1097        self.lastImageType = 1
1098        self.image_display.fitInView()
1099        endts = time.time()
1100        self.singleStatusMsg = "Captured image: {} [{:.3f} s]".format(imgpathname, endts-startts)
1101        self.updateStatusBar()
1102
1103    def _send_file(self, camera_file):
1104        file_data = camera_file.get_data_and_size()
1105        image = Image.open(io.BytesIO(file_data))
1106        image.load()
1107        self.new_image_sig.emit(image)
1108        if self.lastImageType != 0:
1109            self.image_display.fitInView() # reset previous offsets..
1110            self.image_display.resetZoom() # .. then back to zoom_original for preview img
1111            self.lastImageType = 0
1112
1113    @QtCore.pyqtSlot(object)
1114    def new_image(self, image):
1115        self.lastImageSize = image.size
1116        w, h = image.size
1117        image_data = image.tobytes('raw', 'RGB')
1118        self.q_image = QtGui.QImage(image_data, w, h, QtGui.QImage.Format_RGB888)
1119        self._draw_image()
1120        if self.timestamp is not None:
1121            tstampnow = time.time()
1122            tdelta = tstampnow - self.timestamp
1123            fps = 1.0/tdelta
1124            self.fps = "(<{} fps)".format(MINFPS) if (fps<MINFPS) else "{:7.2f} fps".format(fps)
1125            self.timestamp = tstampnow
1126        else:
1127            self.timestamp = time.time()
1128        self.updateStatusBar()
1129
1130    def _draw_image(self):
1131        if not self.q_image:
1132            return
1133        self.pixmap = QtGui.QPixmap.fromImage(self.q_image)
1134        self.image_display.setPhoto(self.pixmap)
1135
1136
1137def main():
1138    # set up logging
1139    # note that: '%(filename)s:%(lineno)d' just prints 'port_log.py:127'
1140    logging.basicConfig(
1141        format='%(asctime)s %(levelname)s: %(name)s: %(message)s', level=logging.WARNING)
1142    callback_obj = gp.check_result(gp.use_python_logging())
1143
1144    # command line argument parser
1145    parser = argparse.ArgumentParser(description="{} - interact with camera via python-gphoto2. Called without command line arguments, it will start a Qt GUI.".format(APPNAME))
1146    parser.add_argument('--save-cam-conf-json', default=argparse.SUPPRESS, help='Get and save the camera configuration to .json file, if standalone (together with --load-cam-conf-json, can be used for copying filtered json files). The string argument is the filename, action is aborted if standalone and no camera online, or if the argument is empty. Overwrites existing files without prompting. Note that if camera is online, but mirror is not raised, process will complete with errors and fewer properties collected in json file (default: suppress)') # "%(default)s"
1147    parser.add_argument('--load-cam-conf-json', default=argparse.SUPPRESS, help='Load and set the camera configuration from .json file, if standalone (together with --save-cam-conf-json, can be used for copying filtered json files). The string argument is the filename, action is aborted if standalone and no camera online, or if the argument is empty (default: suppress)') # "%(default)s"
1148    parser.add_argument('--include-names-json', default=argparse.SUPPRESS, help='Comma separated list of property names to be filtered/included. When using --load-cam-conf-json with --save-cam-conf-json, a json copy with flattening of hierarchy (removal of nodes with children and without value) is performed; in that case --include-names-json can be used to include only certain properties in the output. Can also use `ro=0` or `ro=1` as filtering criteria. If empty ignored (default: suppress)') # "%(default)s"
1149    parser.add_argument('--start-capture-view', default='', help='Command - start capture view (extend lens/raise mirror) on the camera, then exit', action='store_const', const=start_capture_view) # "%(default)s"
1150    parser.add_argument('--stop-capture-view', default='', help='Command - stop capture view (retract lens/release mirror) on the camera, then exit', action='store_const', const=stop_capture_view) # "%(default)s"
1151    parser.add_argument('--config-apply-btn', type=int, default=0, help='GUI option: 0: do not create apply button, update on change; 1: create apply button, update on its click (default: %(default)s)') # ""
1152    args = parser.parse_args() # in case of --help, this also prints help and exits before Qt window is raised
1153    if (args.start_capture_view): args.start_capture_view()
1154    elif (args.stop_capture_view): args.stop_capture_view()
1155    elif (hasattr(args, 'save_cam_conf_json') and not(hasattr(args, 'load_cam_conf_json'))):
1156        getSaveCamConfJson(args)
1157    elif (hasattr(args, 'load_cam_conf_json') and not(hasattr(args, 'save_cam_conf_json'))):
1158        loadSetCamConfJson(args)
1159    elif (hasattr(args, 'load_cam_conf_json') and hasattr(args, 'save_cam_conf_json')):
1160        copyFilterCamConfJson(args)
1161
1162    # start Qt Gui
1163    app = QtWidgets.QApplication([APPNAME]) # SO: 18133302
1164    main = MainWindow(args)
1165    main.show()
1166    sys.exit(app.exec_())
1167
1168
1169if __name__ == "__main__":
1170    main()
1171
1172