From d1da0f36ce61593f931b881d3d34d0f3e13543c2 Mon Sep 17 00:00:00 2001 From: ttrabatt Date: Thu, 9 Jan 2025 00:31:28 +0100 Subject: [PATCH] Added Renumbering Steps to add gaps between renumbered photos --- LOG-PICT_numtool.py.log | 23 + PICT_numtool.py | 15 +- PICT_numtool_241228_save.py | 764 ++++++++++++++++++++ PICT_numtool_ui.py | 12 +- PICT_numtool_ui.ui | 28 +- PICT_numtool_ui_ui.py | 14 +- __pycache__/PICT_numtool_ui.cpython-313.pyc | Bin 0 -> 29751 bytes go | 2 + 8 files changed, 850 insertions(+), 8 deletions(-) create mode 100755 PICT_numtool_241228_save.py create mode 100644 __pycache__/PICT_numtool_ui.cpython-313.pyc create mode 100755 go diff --git a/LOG-PICT_numtool.py.log b/LOG-PICT_numtool.py.log index 17e5148..bc63b02 100755 --- a/LOG-PICT_numtool.py.log +++ b/LOG-PICT_numtool.py.log @@ -3548,3 +3548,26 @@ 01-08 17:26:17|INFO |click_Renumber |535 : Renamed [/Volumes/TIZIANO-SSD1/Don-Ambrogio-50esimo-WIP/Selezione Fiaccolate/PICTtool-tmp/PICTtmp-1slbb8qn] ==> [/Volumes/TIZIANO-SSD1/Don-Ambrogio-50esimo-WIP/Selezione Fiaccolate/19840507-Fiaccolata-Loreto-Final/19840507-Fiaccolata-Orvieto-Final-0082.jpeg] 01-08 17:26:17|INFO |click_Renumber |535 : Renamed [/Volumes/TIZIANO-SSD1/Don-Ambrogio-50esimo-WIP/Selezione Fiaccolate/PICTtool-tmp/PICTtmp-j88qmrnd] ==> [/Volumes/TIZIANO-SSD1/Don-Ambrogio-50esimo-WIP/Selezione Fiaccolate/19840507-Fiaccolata-Loreto-Final/19840507-Fiaccolata-Orvieto-Final-0083.jpeg] 01-08 17:26:17|INFO |click_Renumber |535 : Renamed [/Volumes/TIZIANO-SSD1/Don-Ambrogio-50esimo-WIP/Selezione Fiaccolate/PICTtool-tmp/PICTtmp-7hozo2i2] ==> [/Volumes/TIZIANO-SSD1/Don-Ambrogio-50esimo-WIP/Selezione Fiaccolate/19840507-Fiaccolata-Loreto-Final/19840507-Fiaccolata-Orvieto-Final-0084.jpeg] +01-09 00:06:53|INFO | |765 : PICT_numtool.py: Ver v2.1 - Date: 2023:08:17 +01-09 00:07:23|INFO | |765 : PICT_numtool.py: Ver v2.1 - Date: 2023:08:17 +01-09 00:08:08|INFO | |765 : PICT_numtool.py: Ver v2.1 - Date: 2023:08:17 +01-09 00:08:23|INFO | |765 : PICT_numtool.py: Ver v2.1 - Date: 2023:08:17 +01-09 00:09:34|INFO | |765 : PICT_numtool.py: Ver v2.1 - Date: 2023:08:17 +01-09 00:17:02|INFO | |765 : PICT_numtool.py: Ver v2.1 - Date: 2023:08:17 +01-09 00:20:55|INFO | |765 : PICT_numtool.py: Ver v2.1 - Date: 2023:08:17 +01-09 00:21:29|INFO |click_Open |312 : Reading Metadata from: >1980-Fiaccolata-Milano-010.jpeg< +01-09 00:21:29|INFO |click_Open |312 : Reading Metadata from: >1980-Fiaccolata-Milano-020.jpeg< +01-09 00:21:29|INFO |click_Open |312 : Reading Metadata from: >1980-Fiaccolata-Milano-030.jpeg< +01-09 00:21:29|INFO |click_Open |312 : Reading Metadata from: >1980-Fiaccolata-Milano-040.jpeg< +01-09 00:21:29|INFO |click_Open |312 : Reading Metadata from: >1980-Fiaccolata-Milano-050.jpeg< +01-09 00:21:29|INFO |click_Open |312 : Reading Metadata from: >1980-Fiaccolata-Milano-060.jpeg< +01-09 00:21:50|INFO |click_Renumber |542 : Renamed [/Volumes/TIZIANO-SSD1/ZZ-Don-Ambrogio-50esimo-Finale/Selezione Fiaccolate/PICTtool-tmp/PICTtmp-40sjnw37] ==> [/Volumes/TIZIANO-SSD1/ZZ-Don-Ambrogio-50esimo-Finale/Selezione Fiaccolate/1980-Fiaccolata-Milano copy/20250109-TEST-0001.jpeg] +01-09 00:21:50|INFO |click_Renumber |542 : Renamed [/Volumes/TIZIANO-SSD1/ZZ-Don-Ambrogio-50esimo-Finale/Selezione Fiaccolate/PICTtool-tmp/PICTtmp-s25fn5q2] ==> [/Volumes/TIZIANO-SSD1/ZZ-Don-Ambrogio-50esimo-Finale/Selezione Fiaccolate/1980-Fiaccolata-Milano copy/20250109-TEST-0008.jpeg] +01-09 00:21:50|INFO |click_Renumber |542 : Renamed [/Volumes/TIZIANO-SSD1/ZZ-Don-Ambrogio-50esimo-Finale/Selezione Fiaccolate/PICTtool-tmp/PICTtmp-i3o5mggf] ==> [/Volumes/TIZIANO-SSD1/ZZ-Don-Ambrogio-50esimo-Finale/Selezione Fiaccolate/1980-Fiaccolata-Milano copy/20250109-TEST-0015.jpeg] +01-09 00:21:50|INFO |click_Renumber |542 : Renamed [/Volumes/TIZIANO-SSD1/ZZ-Don-Ambrogio-50esimo-Finale/Selezione Fiaccolate/PICTtool-tmp/PICTtmp-02evf12b] ==> [/Volumes/TIZIANO-SSD1/ZZ-Don-Ambrogio-50esimo-Finale/Selezione Fiaccolate/1980-Fiaccolata-Milano copy/20250109-TEST-0022.jpeg] +01-09 00:21:50|INFO |click_Renumber |542 : Renamed [/Volumes/TIZIANO-SSD1/ZZ-Don-Ambrogio-50esimo-Finale/Selezione Fiaccolate/PICTtool-tmp/PICTtmp-1mhyu4eg] ==> [/Volumes/TIZIANO-SSD1/ZZ-Don-Ambrogio-50esimo-Finale/Selezione Fiaccolate/1980-Fiaccolata-Milano copy/20250109-TEST-0029.jpeg] +01-09 00:21:50|INFO |click_Renumber |542 : Renamed [/Volumes/TIZIANO-SSD1/ZZ-Don-Ambrogio-50esimo-Finale/Selezione Fiaccolate/PICTtool-tmp/PICTtmp-x1fdhfij] ==> [/Volumes/TIZIANO-SSD1/ZZ-Don-Ambrogio-50esimo-Finale/Selezione Fiaccolate/1980-Fiaccolata-Milano copy/20250109-TEST-0036.jpeg] +01-09 00:23:20|INFO | |765 : PICT_numtool.py: Ver v2.1 - Date: 2023:08:17 +01-09 00:26:34|INFO | |765 : PICT_numtool.py: Ver v2.1 - Date: 2023:08:17 +01-09 00:27:23|INFO | |765 : PICT_numtool.py: Ver v2.1 - Date: 2023:08:17 +01-09 00:28:20|INFO | |765 : PICT_numtool.py: Ver v2.1 - Date: 2023:08:17 diff --git a/PICT_numtool.py b/PICT_numtool.py index c1ffb09..3d3b80d 100755 --- a/PICT_numtool.py +++ b/PICT_numtool.py @@ -13,6 +13,7 @@ Tiziano Trabattoni | 2023/05/22 | Porting su MacOS per pyqt6 Tiziano Trabattoni | 2023/08/14 | Sorting by Creation date with exiftool Tiziano Trabattoni | 2024/08/19 | Changed movie tags to include filemodification date for .mp4 from Android Tiziano Trabattoni | 2024/12/28 | Introduced possibility to ignore metadata and sort only by filename for Undetermined Metadata (old slides) +Tiziano Trabattoni | 2025/01/08 | Added steps for numbering ####### Developed under poetry (with pyqt6 M1 ARM64 code available) ~/Develop/Personal/PICT_poetry » poetry remove pyqt5 @@ -49,6 +50,10 @@ from collections import OrderedDict ''' PyQT6 imports''' from PICT_numtool_ui import Ui_MainWindow +# from PyQt import QtGui +# from PyQt.QtWidgets import QTreeView, QLabel, QTreeWidgetItem, QMainWindow, QApplication, QFileDialog, QAbstractItemView +# from PyQt.QtGui import QPixmap +from PICT_numtool_ui import Ui_MainWindow from PyQt6 import QtGui from PyQt6.QtWidgets import QTreeView, QLabel, QTreeWidgetItem, QMainWindow, QApplication, QFileDialog, QAbstractItemView from PyQt6.QtGui import QPixmap @@ -98,7 +103,7 @@ BCOL_BLACK = "background-color: black;" ''' Default instances ''' DEF_INSTANCE = 'select' -NON_FILE_CHARACTERS = '\|/*$?:;<>+"''' +NON_FILE_CHARACTERS = '\\|/*$?:;<>+\"\'' ''' Ordering dictionary ''' WorkData = {} @@ -189,7 +194,9 @@ class main(QMainWindow): self.ui.cmb_Month.addItem(str(i)) for i in range(YEARS_RANGE[0],YEARS_RANGE[1]+1): self.ui.cmb_Year.addItem(str(i)) - + for i in range(1,11): + self.ui.cmb_Step.addItem(str(i)) + ''' set date and time ''' # print datetime.now().strftime('%d/%m/%Y') self.day = int(datetime.now().strftime('%d')) @@ -544,7 +551,7 @@ class main(QMainWindow): 'rename' : True }, }) - count += 1 + count += int(self.ui.cmb_Step.currentText()) self.pb.end() @@ -722,7 +729,7 @@ class PB(): actv = self.progressbar.value() newv = actv + self.step if newv <= self.max: - self.progressbar.setValue(newv) + self.progressbar.setValue(int(newv)) '''==========================================================================================================================''' diff --git a/PICT_numtool_241228_save.py b/PICT_numtool_241228_save.py new file mode 100755 index 0000000..c1ffb09 --- /dev/null +++ b/PICT_numtool_241228_save.py @@ -0,0 +1,764 @@ +#!/usr/bin/python +''' +========================================================================================================= +T&T Software and Systems +Project: Renumbering tool +Module: Main module +Description: Tool to renumber ohoto and video replace VB one + + +Tiziano Trabattoni | 2016/12/10 | first version +Tiziano Trabattoni | 2022/07/31 | Porting su MacOS, PyQt5, python3 +Tiziano Trabattoni | 2023/05/22 | Porting su MacOS per pyqt6 +Tiziano Trabattoni | 2023/08/14 | Sorting by Creation date with exiftool +Tiziano Trabattoni | 2024/08/19 | Changed movie tags to include filemodification date for .mp4 from Android +Tiziano Trabattoni | 2024/12/28 | Introduced possibility to ignore metadata and sort only by filename for Undetermined Metadata (old slides) + +####### Developed under poetry (with pyqt6 M1 ARM64 code available) +~/Develop/Personal/PICT_poetry » poetry remove pyqt5 +~/Develop/Personal/PICT_poetry » poetry add pyqt6 + +~/Develop/Personal/PICT_poetry » pip freeze ttrabatt@TTRABATT-M-3QXG +PyQt6==6.5.0 +PyQt6-Qt6==6.5.0 +PyQt6-sip==13.5.1 +(pict-numtool-py3.9) ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +~/Develop/Personal/PICT_poetry » poetry show ttrabatt@TTRABATT-M-3QXG +pyqt6 6.5.0 Python bindings for the Qt cross platform application toolkit +pyqt6-qt6 6.5.0 The subset of a Qt installation needed by PyQt6. +pyqt6-sip 13.5.1 The sip module support for PyQt6 +(pict-numtool-py3.9) ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +~/Develop/Personal/PICT_poetry » + + + +========================================================================================================= +''' +import sys +import os +import shutil +import time +import logging +import threading +import subprocess +import tempfile +import json +from datetime import datetime +from collections import OrderedDict + + +''' PyQT6 imports''' +from PICT_numtool_ui import Ui_MainWindow +from PyQt6 import QtGui +from PyQt6.QtWidgets import QTreeView, QLabel, QTreeWidgetItem, QMainWindow, QApplication, QFileDialog, QAbstractItemView +from PyQt6.QtGui import QPixmap +#from PyQt5.QtCore import * +#from PyQt6 import QtCore +global LOGGER + +''' Module version ''' +mod_ver = "v2.1" +mod_dver = "2023:08:17" + +''' Logger Variables ''' +LOG_FORMAT = '%(asctime)s|%(levelname)-7s|%(funcName)-15s|%(lineno)-5d: %(message)-50s' +LOG_TIME_FORMAT = '%m-%d %H:%M:%S' + +CONSOLE_DEBUG = logging.INFO +FILE_DEBUG = logging.INFO + +PHOTO_filter = 'Images (*.jpg *.jpeg *.png *.tiff *.heic, *.tif)' +MOVIE_filter = 'Movies (*.mov *.mp4)' + +# file format and extension variables +EXIFTOOL = "exiftool" +EXIF_PHOTO_EXT = (".jpg", ".JPG", ".jpeg", ".JPEG", ".png", ".PNG", ".heic", ".HEIC", ".tiff", ".TIFF", ".tif", ".TIF") +EXIF_MOVIE_EXT = (".mov", ".MOV", ".mp4", ".MP4",) +EXIF_PHOTO_TAGS = ("-datetimeoriginal", "-filecreatedate", "-filemodifydate",) +EXIF_MOVIE_TAGS = ("-mediacreatedate","-filemodifydate",) + +FILE_TREE_CNTL = [('Filename.', 300, QTreeView), + ('FullNAme', 800, QtGui.QTextItem), ('--', 20, QLabel)] + +YEARS_RANGE = [1970, 2040] + +COL_WHITE = "QPushButton { color: white;}" +COL_RED = "QPushButton { color: red;}" +COL_GREEN = "QPushButton { color: green;}" +COL_YELLOW = "QPushButton { color: yellow;}" +COL_BLUE = "QPushButton { color: blue;}" +COL_BLACK = "QPushButton { color: black;}" + +BCOL_WHITE = "background-color: white;" +BCOL_RED = "background-color: red;" +BCOL_GREEN = "background-color: green;" +BCOL_YELLOW = "background-color: yellow;" +BCOL_CYAN = "background-color: cyan;" +BCOL_BLACK = "background-color: black;" + +''' Default instances ''' +DEF_INSTANCE = 'select' +NON_FILE_CHARACTERS = '\|/*$?:;<>+"''' + +''' Ordering dictionary ''' +WorkData = {} +WorkDir = '' +TmpDir = '' + +class PICTitem(QTreeWidgetItem): + ''' + Custom QTreeWidgetItem with Widgets + ''' + _basename = "" + _name = "" + + @property + def basename(self): + return self._basename + + @property + def name(self): + return self._name + + def __init__(self, parent, basename, name): + ''' + parent (QTreeWidget) : Item's QTreeWidget parent. + value (str) : Item's name. just an example. + ''' + + ## Init super class ( QtGui.QTreeWidgetItem ) + super(PICTitem, self).__init__(parent) + + ''' ''' + self.setText(0, basename) + #self.setTextAlignment(0, QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) + self.setText(1, name) + #self.setTextAlignment(1, QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) + self._name=name + self._basename=basename + + def __repr__(self) -> str: + return str(self) + + def __str__(self): + return f" Filename:{self.basename}" + + +''' ======================== MAIN WINDOW CLASS with all methods ================================ ''' +class main(QMainWindow): + def __init__(self): + QMainWindow.__init__(self) + self.ui = Ui_MainWindow() + self.ui.setupUi(self) + + ''' create Progressbar element ''' + self.pb = PB(self.ui.pb_Run,mi = 0, ma =100) + + ''' set default values ''' + self.day = 0 + self.month = 0 + self.year = 1900 + self.name = '' + self.fullName = '' + self.ineditname = False + self.filecriteria = PHOTO_filter + self.TmpDir = '' + self.WorkDir = '' + self.WorkData = {} + self.toRename = [] + + + ''' set the tree_OUT widget''' + self.ui.tree_OUT.setColumnCount(len(FILE_TREE_CNTL)) + self.ui.tree_OUT.setHeaderLabels(self.set_qtree_headers(FILE_TREE_CNTL)) + self.ui.tree_OUT.setUniformRowHeights(True) + #self.ui.tree_OUT.setSelectionMode(QabsW.ExtendedSelection) + + ''' set all Methods ''' + self.ui.btn_Renumber.setEnabled(False) + self.ui.btn_Renumber.clicked.connect(self.click_Renumber) + self.ui.btn_Open.clicked.connect(self.click_Open) + self.ui.btn_Exit.clicked.connect(self.click_Exit) + #self.ui.txt_Name.editingFinished.connect(self.edited_Name) + self.ui.rbtn_Photo.setChecked(True) + + ''' set all combo ''' + for i in range(1,32): + self.ui.cmb_Day.addItem(str(i)) + for i in range(1,13): + self.ui.cmb_Month.addItem(str(i)) + for i in range(YEARS_RANGE[0],YEARS_RANGE[1]+1): + self.ui.cmb_Year.addItem(str(i)) + + ''' set date and time ''' + # print datetime.now().strftime('%d/%m/%Y') + self.day = int(datetime.now().strftime('%d')) + self.month = int(datetime.now().strftime('%m')) + self.year = int(datetime.now().strftime('%Y')) + self.ui.cmb_Day.setCurrentIndex(self.day - 1) + self.ui.cmb_Month.setCurrentIndex(self.month - 1) + self.ui.cmb_Year.setCurrentIndex(self.year - YEARS_RANGE[0]) + self.ui.date_Day.setDate(datetime.now()) + self.ui.txt_Name.textChanged.connect(self.changed_Name) + + ######## These are OLD PyQT4 signal sintax ########### + #''' set Object signals ''' + #''' visualization selection via Radio Button ''' + #self.ui.rbtn_Movies.connect(self.ui.rbtn_Movies, QtCore.SIGNAL("clicked()"),self.selected_rbtn_Movies) + #self.ui.rbtn_Photo.connect(self.ui.rbtn_Photo, QtCore.SIGNAL("clicked()"), self.selected_rbtn_Photos) + #self.ui.date_Day.connect(self.ui.date_Day, QtCore.SIGNAL("dateChanged(QDate)"), self.change_date) +# + #''' Signals ''' + #self.ui.cmb_Day.connect(self.ui.cmb_Day, QtCore.SIGNAL("currentIndexChanged(const QString&)"), self.choose_Day) + #self.ui.cmb_Month.connect(self.ui.cmb_Month, QtCore.SIGNAL("currentIndexChanged(const QString&)"), self.choose_Month) + #self.ui.cmb_Year.connect(self.ui.cmb_Year, QtCore.SIGNAL("currentIndexChanged(const QString&)"), self.choose_Year) + ## self.ui.txt_Name.connect(self.ui.txt_Name, QtCore.SIGNAL("insertPlainText (const Qstring&"), self.changed_Name) + + ''' set Object signals ''' + ''' visualization selection via Radio Button ''' + self.ui.rbtn_Movies.clicked.connect(self.selected_rbtn_Movies) + self.ui.rbtn_Photo.clicked.connect(self.selected_rbtn_Photos) + self.ui.date_Day.dateChanged.connect(self.change_date) + self.ui.tree_OUT.itemSelectionChanged.connect(self.on_selectionChanged) + + ''' Signals ''' + self.ui.cmb_Day.currentIndexChanged.connect(self.choose_Day) + self.ui.cmb_Month.currentIndexChanged.connect(self.choose_Month) + self.ui.cmb_Year.currentIndexChanged.connect(self.choose_Year) + + ''' other setting ''' + self.ui.pb_Run.setMinimum(0) + self.ui.pb_Run.setMaximum(100) + self.ui.pb_Run.setVisible(False) + self.ui.chk_IgnMeta.setChecked(False) + + def click_Exit(self): + sys.exit(0) + return + + def QaddFILEChild(self, parent, basename, name): + item = PICTitem(parent, basename, name) + return item + + + def on_selectionChanged(self): + selected = [ s.basename for s in self.ui.tree_OUT.selectedItems() ] + for f in self.WorkData: + if f in selected: + self.WorkData[f]['rename'] = True + else: + self.WorkData[f]['rename'] = False + return + + + def click_Open(self): + ''' open source directory ''' + #files = QFileDialog.getOpenFileNames(self, "Open Directory", "" , self.filecriteria, None, QFileDialog.DontUseNativeDialog) + files = QFileDialog.getOpenFileNames(self, "Open Directory", "" , self.filecriteria, None) + nf = len(files) + # SHIT now QfileDialog return a Tuple instead of list + # Has to be addressed as [0] + # because files[1] contains the Type: , - Value >Images (*.jpg *.jpeg *.png *.tiff) + + + if len(files[0]) > 0: + ''' clear and set the tree_OUT widget''' + self.ui.tree_OUT.clear() + self.ui.tree_OUT.setColumnCount(len(FILE_TREE_CNTL)) + self.ui.tree_OUT.setHeaderLabels(self.set_qtree_headers(FILE_TREE_CNTL)) + self.ui.tree_OUT.setUniformRowHeights(True) + + self.pb.start(steps=nf) + + ''' initialized work list dictionary ''' + self.WorkData = {} + + # LOGGER.debug("Type of files[] >%s<" % (str(type(files)))) + # self.WorkDir = os.path.dirname(str(files[0])) + # SHIT now QfileDialog return a Tuple instead of list + # Has to be addressed as [0] + # because files[1] contains the Type: , - Value >Images (*.jpg *.jpeg *.png *.tiff) + fileuno = files[0][0] + #LOGGER.debug("fileuno >%s<" % fileuno) + + self.WorkDir = os.path.dirname(fileuno) + LOGGER.debug(f"Workdir >{self.WorkDir}<") + + ''' tempory dir must be on same disk otherwise is too slow''' + basedir = os.path.dirname(self.WorkDir) + self.TmpDir = basedir + '/PICTtool-tmp' + if not os.path.exists(self.TmpDir): + os.mkdir(self.TmpDir) + count = 1 + + #for fname in files[0]: + # LOGGER.debug("Type: %s, - Value >%s<" % (type(fname), fname)) + + ''' for all selected files ''' + for fname in files[0]: + self.pb.incr() + self.repaint() + ''' add an item to Qtree including resizing of pictures ''' + # self.add_Qtree_item(fname) + + basename = os.path.basename(str(fname)) + LOGGER.info(f"Reading Metadata from: >{basename}< ") + + # Get File Metadata via external exiftool + # requires installation + # https://exiftool.org/index.html#running + # + + if self.ui.chk_IgnMeta: + # dont look at meta data of file just sort for name + ''' fill dictionary ''' + self.WorkData.update({ + # int(time.mktime(time.strptime(dt, '%Y:%m:%d|%H:%M:%S'))) : + basename : + { + #'itemNum' : str(count), + 'fname' : basename, + 'fileExt' : self.get_extension(str(fname)), + 'tmpName' : self.TmpDir + '/PICTtmp-' + next(tempfile._get_candidate_names()), + 'rename' : True, + 'date' : "", + 'time' : "" + } + }) + count += 1 + + + else: + # Normal way looking at Metadata to get file sosted with creation time + + # exiftool has produced with json and TAGS + if basename.endswith(EXIF_MOVIE_EXT): + EXIF_TAGS = EXIF_MOVIE_TAGS + else: + EXIF_TAGS = EXIF_PHOTO_TAGS + + args=[EXIFTOOL, "-json", *EXIF_TAGS, os.path.join(self.WorkDir,basename)] + LOGGER.debug(str(args)) + process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + res=json.loads(process.stdout.read()) + LOGGER.debug(json.dumps(res,indent=2)) + dat = res[0] + + # reset of all tag values + dt = odate = fmdate = fcdate = mcdate = None + if basename.endswith(EXIF_MOVIE_EXT): + # MOVIEs + try: + fmdate = dat['FileModifyDate'] + except KeyError: + LOGGER.debug(f"File {basename} does not contain tag FileModifyDate") + except Exception as e: + LOGGER.error(f"File {basename} parsing error {e}") + continue # for fname in files[0]: + + try: + mcdate = dat['MediaCreateDate'] + except KeyError: + LOGGER.debug(f"File {basename} does not contain tag MediaCreateDate") + except Exception as e: + LOGGER.error(f"File {basename} parsing error {e}") + continue + + if not mcdate and not fmdate: + LOGGER.error(f"File {basename} does NOT contain expected metadata tags, please verify results") + continue # for fname in files[0]: + + + if fmdate: + try: + # dt = f"{mcdate[0]}:{mcdate[1]}:{mcdate[2].split(' ')[0]}|{mcdate[2].split(' ')[1]}:{mcdate[3]}:{(mcdate[4].split('+'))[0]}".strip() + pd = int(time.mktime(time.strptime(fmdate, '%Y:%m:%d %H:%M:%S%z'))) + pod = datetime.fromtimestamp(pd) + dt = pod.strftime('%Y:%m:%d|%H:%M:%S') + except Exception as e: + LOGGER.error(f"File {basename} parsing tag value: {e} ...Skipping...") + continue # for fname in files[0]: + else: + try: + # dt = f"{mcdate[0]}:{mcdate[1]}:{mcdate[2].split(' ')[0]}|{mcdate[2].split(' ')[1]}:{mcdate[3]}:{(mcdate[4].split('+'))[0]}".strip() + pd = int(time.mktime(time.strptime(mcdate, '%Y:%m:%d %H:%M:%S'))) + pod = datetime.fromtimestamp(pd) + dt = pod.strftime('%Y:%m:%d|%H:%M:%S') + except Exception as e: + LOGGER.error(f"File {basename} parsing tag value: {e} ...Skipping...") + continue # for fname in files[0]: + else: + # PICTUREs + try: + odate = dat['DateTimeOriginal'] + except KeyError: + LOGGER.debug(f"File {basename} does not contain tag DateTimeOriginal") + except Exception as e: + LOGGER.error(f"File {basename} parsing error {e}") + continue # for fname in files[0]: + + try: + fcdate = dat['FileCreateDate'] + except KeyError: + LOGGER.debug(f"File {basename} does not contain tag FileCreateDate") + except Exception as e: + LOGGER.error(f"File {basename} parsing error {e}") + continue # for fname in files[0]: + + try: + fmdate = dat['FileModifyDate'] + except KeyError: + LOGGER.debug(f"File {basename} does not contain tag FileModifyDate") + except Exception as e: + LOGGER.error(f"File {basename} parsing error {e}") + continue # for fname in files[0]: + + if not odate and not fcdate and not fmdate: + LOGGER.error(f"File {basename} does NOT contain expected metadata tags, please verify results") + continue # for fname in files[0]: + + if odate: + #take DateTimeOriginal + od = int(time.mktime(time.strptime(odate, '%Y:%m:%d %H:%M:%S'))) + ood = datetime.fromtimestamp(od) + dt = ood.strftime('%Y:%m:%d|%H:%M:%S') + elif fcdate and fmdate: + # take older between FileCreateDate and FileModifyDate + fcfmdate = min(int(time.mktime(time.strptime(fcdate, '%Y:%m:%d %H:%M:%S%z'))), int(time.mktime(time.strptime(fmdate, '%Y:%m:%d %H:%M:%S%z')))) + ofcfmdate = datetime.fromtimestamp(fcfmdate) + dt = ofcfmdate.strftime('%Y:%m:%d|%H:%M:%S') + elif fcdate and not fmdate: + # take fcdate + fc = int(time.mktime(time.strptime(fcdate, '%Y:%m:%d %H:%M:%S%z'))) + dt = fc.strftime('%Y:%m:%d|%H:%M:%S') + else: + # take fmdate + fm = int(time.mktime(time.strptime(fmdate, '%Y:%m:%d %H:%M:%S%z'))) + dt = fm.strftime('%Y:%m:%d|%H:%M:%S') + + if not dt: + LOGGER.error(f"File {basename} does NOT contain expected metadata tags, please verify results") + continue # for fname in files[0]: + + # check if same modification time exist so it loops adding one second to modification time + adt = int(time.mktime(time.strptime(dt, '%Y:%m:%d|%H:%M:%S'))) + + while adt in self.WorkData.keys(): + adt+=1 + + LOGGER.info(f"Time: {dt} --> {basename}") + + ''' fill dictionary ''' + self.WorkData.update({ + # int(time.mktime(time.strptime(dt, '%Y:%m:%d|%H:%M:%S'))) : + adt : + { + #'itemNum' : str(count), + 'fname' : basename, + 'fileExt' : self.get_extension(str(fname)), + 'tmpName' : self.TmpDir + '/PICTtmp-' + next(tempfile._get_candidate_names()), + 'rename' : True, + 'date': dt.split('|')[0], + 'time': dt.split('|')[1] + } + }) + count += 1 + # end of sorting with meta data + self.pb.end() + # print unsorted dictionary + # print(f"{json.dumps(self.WorkData, indent=4)}") + + # transpose disctionary and sort it by date of creation of the file + flts = dict(sorted(self.WorkData.items())) + # self.WorkData = flts + + # print(f"{json.dumps(self.WorkData, indent=4)}") + + for _,v in self.WorkData.items(): + ''' add an item to Qtree including resizing of pictures ''' + # self.add_Qtree_item(f"{v['fname']}-->{v['date']}|{v['time']}") + self.add_Qtree_item(f"{v['fname']}") + + return + + def click_Renumber(self): + ''' renumber and create a new WorkData for next phases ''' + newWorkData = {} + + ''' only if WorkData Contains elements ''' + nf = len(self.WorkData) + if nf > 0: + self.pb.start(steps=nf * 2) + + ''' for all files move to temp to avoid overlapping''' + for _,v in self.WorkData.items(): + LOGGER.debug(f"Moving file {v}") + if v['rename']: + self.pb.incr() + try: + tmpfile = v['tmpName'] + origfile = self.WorkDir + '/' + v['fname'] + except Exception as e: + LOGGER.error(f"Parsing file attributes [{v['fname']} : {e} ... Skipping ...") + else: + try: + shutil.move(origfile, tmpfile) + #os.rename(origfile, tmpfile) + except Exception as e: + LOGGER.error(f"Renaming file [{origfile}] to tmpfile: {tmpfile} : {e} ... Skipping ...") + else: + # LOGGER.debug(f"Origfile [{origfile}] ==> TmpFile [{tmpfile}]') + pass + + ''' for all files move to current directory with new name''' + count = 1 + for _,v in self.WorkData.items(): + LOGGER.debug(f"Moving file {v}") + if v['rename']: + self.pb.incr() + try: + tmpfile = v['tmpName'] + extension = v['fileExt'] + #count = int(self.WorkData[fname]['itemNum']) + except Exception as e: + LOGGER.error(f"Parsing file attributes [{v['fname']} : {e} ... Skipping ...") + else: + targetfile = self.WorkDir + '/' + self.fullName + '-{0:04d}'.format(count) + extension + targetfname = self.fullName + '-{0:04d}'.format(count) + extension + try: + shutil.move(tmpfile, targetfile) + #os.rename(tmpfile, targetfile) + except Exception as e: + LOGGER.error(f"Renaming tmpfile [{tmpfile}] to: {targetfname} : {e} ... Skipping ...") + else: + #LOGGER.debug('TmpFile [%s] ==> TargetFIle [%s]' % (tmpfile, targetfile)) + LOGGER.info(f"Renamed [{tmpfile}] ==> [{targetfile}]") + pass + + ''' fill dictionary ''' + newWorkData.update({ + targetfname : { + 'itemNum' : str(count), + 'fileExt' : self.get_extension(str(targetfname)), + 'tmpName' : self.TmpDir + '/PICTtmp-' + next(tempfile._get_candidate_names()), + 'rename' : True + }, + }) + count += 1 + + self.pb.end() + + ''' also rename directory ''' + basedir = os.path.dirname(self.WorkDir) + olddir = self.WorkDir + newdir = basedir + '/' + self.fullName + try: + shutil.move(olddir, newdir) + except Exception as e: + LOGGER.error(f"Rename of directory to [{basedir}] failed {e} ...Skipping...") + + ''' sel new Current working directory and Workdata to new dictionary ''' + self.WorkDir = newdir + os.chdir(self.WorkDir) + self.WorkData = newWorkData + + ''' empties Qtree ''' + self.ui.tree_OUT.clear() + self.ui.tree_OUT.setColumnCount(len(FILE_TREE_CNTL)) + self.ui.tree_OUT.setHeaderLabels(self.set_qtree_headers(FILE_TREE_CNTL)) + self.ui.tree_OUT.setUniformRowHeights(True) + + ''' and refill it again ''' + nf = len (self.WorkData) + WorkData_s = OrderedDict(self.WorkData) + + # redraw the dialog + self.pb.start(steps=nf) + for fname in WorkData_s: + self.pb.incr() + self.add_Qtree_item(self.WorkDir + '/' + fname) + + self.pb.end() + + return + + def add_Qtree_item(self, fname): + basefile = os.path.basename(str(fname)) + + if self.ui.chk_Preview.isChecked(): + ''' review is activated need to elaborate thumbnail ''' + item = self.QaddFILEChild(self.ui.tree_OUT, basefile, str(fname)) + try: + ''' rezize file ''' + #img = QtGui.QImage(fname) + # qim = img.scaled(64, 64, QtCore.Qt.KeepAspectRatioByExpanding, QtCore.Qt.SmoothTransformation) + # qim = img.scaled(64, 64) + # pict = QPixmap.fromImage(qim) + pict = QPixmap(fname) + except Exception as e: + LOGGER.error(f"Unable to process image [{fname}] {e} ... Skipping ...") + label = QLabel() + label.setText("Preview Error") + else: + ''' create a Label with picture as background ''' + label = QLabel() + label.setPixmap(pict) + + ''' add column 2 a Label with picture in pixmap ''' + item.treeWidget().setItemWidget(item, 2, label) + else: + item = self.QaddFILEChild(self.ui.tree_OUT, basefile, str(fname)) + return item + + def get_extension(self, fname): + basename = os.path.basename(fname) # os independent + ext = '.'.join(basename.split('.')[1:]) + return '.' + ext if ext else None + + def change_date(self): + self.day = self.ui.date_Day.date().day() + self.month = self.ui.date_Day.date().month() + self.year = self.ui.date_Day.date().year() + self.ui.cmb_Day.setCurrentIndex(self.day - 1) + self.ui.cmb_Month.setCurrentIndex(self.month - 1) + self.ui.cmb_Year.setCurrentIndex(self.year - YEARS_RANGE[0] ) + + ''' set fullname ''' + self.set_fullName() + + return + + def changed_Name(self): + if self.ineditname: + return + else: + tc = self.ui.txt_Name.textCursor() + name = str(self.ui.txt_Name.toPlainText()) + filename = "".join(i for i in name if i not in NON_FILE_CHARACTERS) + self.ineditname = True + self.ui.txt_Name.setPlainText(filename) + self.ui.txt_Name.setTextCursor(tc) + self.ineditname = False + self.name = filename + self.set_fullName() + return + + def selected_rbtn_Movies(self): + self.filecriteria = MOVIE_filter + return + + def selected_rbtn_Photos(self): + self.filecriteria = PHOTO_filter + return + + def choose_Day(self): + self.day = int(self.ui.cmb_Day.currentText()) + self.set_fullName() + return + + def choose_Month(self): + self.month = int(self.ui.cmb_Month.currentText()) + self.set_fullName() + return + + def choose_Year(self): + self.year = int(self.ui.cmb_Year.currentText()) + self.set_fullName() + return + + def set_qtree_headers(self, tree_ctnl): + tree_HEADERS = [] + i = 0 + for treeitem in tree_ctnl: + tree_HEADERS.append(treeitem[0]) + self.ui.tree_OUT.setColumnWidth(i, treeitem[1]) + i += 1 + return tree_HEADERS + + def set_fullName(self): + self.fullName = '{0:4d}{1:02d}{2:02d}-{3:s}'.format(self.year, self.month, self.day, self.name) + self.ui.txt_FullName.setText(self.fullName) + if len(self.fullName) > 9: + self.ui.btn_Renumber.setEnabled(True) + else: + self.ui.btn_Renumber.setEnabled(False) + + # def msg_print(self, msg): + # self.ui.txt_Error.setText(str(msg)) + # self.repaint() + +''' ========================= Other Functions ===================================================''' +''' progress bar class and methods ''' +class PB(): + def __init__(self, progressbar, mi = 0, ma = 100 ): + # super(PB, self).__init__(parent) + self.val = 0 + self.min = mi + self.max = ma + self.steps = 10 + self.step = (self.max - self.min)/self.steps + self.progressbar = progressbar + self.progressbar.setMinimum(self.min) + self.progressbar.setMaximum(self.max) + + def start(self, steps=10): + self.steps = (steps if steps > 0 else 10) + self.progressbar.setVisible(True) + self.progressbar.setValue(0) + + def end(self): + self.progressbar.setValue(self.max) + # start a timer to make progressbar invisible + threading.Timer(3,self.clear).start() + + def clear(self): + self.progressbar.setVisible(False) + + def upd(self, val): + self.val = val + self.progressbar.setValue(self.val) + + def incr(self): + actv = self.progressbar.value() + newv = actv + self.step + if newv <= self.max: + self.progressbar.setValue(newv) + + +'''==========================================================================================================================''' +''' Main Program ''' +if __name__ == '__main__': + + ''' Enabling Logger + ''' + prog_name = os.path.basename(sys.argv[0:][0]) + LOGGER = logging.getLogger(__name__) + LOGGER.setLevel(logging.DEBUG) + LOGGER.propagate = False + logging.captureWarnings(True) + formatter = logging.Formatter(LOG_FORMAT, LOG_TIME_FORMAT) + + ''' File logging + ''' + log_name = "LOG-" + prog_name + ".log" + fh = logging.FileHandler(log_name) + fh.setLevel(FILE_DEBUG) + fh.setFormatter(formatter) + LOGGER.addHandler(fh) + + ''' Console logging + ''' + cl = logging.StreamHandler(sys.stdout) + cl.setLevel(CONSOLE_DEBUG) + cl.setFormatter(formatter) + LOGGER.addHandler(cl) + + time.sleep(1) + + LOGGER.info("%s: Ver %s - Date: %s" % (prog_name, mod_ver, mod_dver)) + + ''' Open window and give control to QTPY ''' + app = QApplication(sys.argv) + window = main() + window.show() + sys.exit(app.exec()) diff --git a/PICT_numtool_ui.py b/PICT_numtool_ui.py index 58f8578..b1e5aaa 100755 --- a/PICT_numtool_ui.py +++ b/PICT_numtool_ui.py @@ -201,13 +201,22 @@ class Ui_MainWindow(object): sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.chk_Preview.sizePolicy().hasHeightForWidth()) self.chk_Preview.setSizePolicy(sizePolicy) - self.chk_Preview.setMinimumSize(QtCore.QSize(150, 20)) + self.chk_Preview.setMinimumSize(QtCore.QSize(110, 20)) self.chk_Preview.setObjectName("chk_Preview") self.horizontalLayout.addWidget(self.chk_Preview) self.btn_Open = QtWidgets.QPushButton(parent=self.wdg_Main) self.btn_Open.setMinimumSize(QtCore.QSize(100, 30)) self.btn_Open.setObjectName("btn_Open") self.horizontalLayout.addWidget(self.btn_Open) + self.lbl_Step = QtWidgets.QLabel(parent=self.wdg_Main) + self.lbl_Step.setMaximumSize(QtCore.QSize(120, 16777215)) + self.lbl_Step.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) + self.lbl_Step.setObjectName("lbl_Step") + self.horizontalLayout.addWidget(self.lbl_Step) + self.cmb_Step = QtWidgets.QComboBox(parent=self.wdg_Main) + self.cmb_Step.setMaximumSize(QtCore.QSize(60, 16777215)) + self.cmb_Step.setObjectName("cmb_Step") + self.horizontalLayout.addWidget(self.cmb_Step) self.btn_Renumber = QtWidgets.QPushButton(parent=self.wdg_Main) self.btn_Renumber.setMinimumSize(QtCore.QSize(100, 0)) self.btn_Renumber.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus) @@ -259,5 +268,6 @@ class Ui_MainWindow(object): self.chk_IgnMeta.setText(_translate("MainWindow", "Ignore Metadata")) self.chk_Preview.setText(_translate("MainWindow", "Show Preview")) self.btn_Open.setText(_translate("MainWindow", "Open Folder")) + self.lbl_Step.setText(_translate("MainWindow", "Numbering Steps")) self.btn_Renumber.setText(_translate("MainWindow", "Renumber Photo and Movies")) self.btn_Exit.setText(_translate("MainWindow", "Exit")) diff --git a/PICT_numtool_ui.ui b/PICT_numtool_ui.ui index 27877e7..5090e61 100755 --- a/PICT_numtool_ui.ui +++ b/PICT_numtool_ui.ui @@ -429,7 +429,7 @@ - 150 + 110 20 @@ -451,6 +451,32 @@ + + + + + 120 + 16777215 + + + + Numbering Steps + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 60 + 16777215 + + + + diff --git a/PICT_numtool_ui_ui.py b/PICT_numtool_ui_ui.py index cb5675a..27199c7 100644 --- a/PICT_numtool_ui_ui.py +++ b/PICT_numtool_ui_ui.py @@ -1,4 +1,4 @@ -# Form implementation generated from reading ui file '/Users/Tiziano/Library/CloudStorage/OneDrive-Personal/Documents/Develop/Develop-Personal/PICT_poetry_devel/PICT_numtool_ui.ui' +# Form implementation generated from reading ui file '/Users/Tiziano/Library/CloudStorage/OneDrive-Personal/Documents/Develop/Develop-Personal/PICT_numtool_NG/PICT_numtool_ui.ui' # # Created by: PyQt6 UI code generator 6.7.1 # @@ -201,13 +201,22 @@ class Ui_MainWindow(object): sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.chk_Preview.sizePolicy().hasHeightForWidth()) self.chk_Preview.setSizePolicy(sizePolicy) - self.chk_Preview.setMinimumSize(QtCore.QSize(150, 20)) + self.chk_Preview.setMinimumSize(QtCore.QSize(110, 20)) self.chk_Preview.setObjectName("chk_Preview") self.horizontalLayout.addWidget(self.chk_Preview) self.btn_Open = QtWidgets.QPushButton(parent=self.wdg_Main) self.btn_Open.setMinimumSize(QtCore.QSize(100, 30)) self.btn_Open.setObjectName("btn_Open") self.horizontalLayout.addWidget(self.btn_Open) + self.lbl_Step = QtWidgets.QLabel(parent=self.wdg_Main) + self.lbl_Step.setMaximumSize(QtCore.QSize(120, 16777215)) + self.lbl_Step.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) + self.lbl_Step.setObjectName("lbl_Step") + self.horizontalLayout.addWidget(self.lbl_Step) + self.cmb_Step = QtWidgets.QComboBox(parent=self.wdg_Main) + self.cmb_Step.setMaximumSize(QtCore.QSize(60, 16777215)) + self.cmb_Step.setObjectName("cmb_Step") + self.horizontalLayout.addWidget(self.cmb_Step) self.btn_Renumber = QtWidgets.QPushButton(parent=self.wdg_Main) self.btn_Renumber.setMinimumSize(QtCore.QSize(100, 0)) self.btn_Renumber.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus) @@ -259,5 +268,6 @@ class Ui_MainWindow(object): self.chk_IgnMeta.setText(_translate("MainWindow", "Ignore Metadata")) self.chk_Preview.setText(_translate("MainWindow", "Show Preview")) self.btn_Open.setText(_translate("MainWindow", "Open Folder")) + self.lbl_Step.setText(_translate("MainWindow", "Numbering Steps")) self.btn_Renumber.setText(_translate("MainWindow", "Renumber Photo and Movies")) self.btn_Exit.setText(_translate("MainWindow", "Exit")) diff --git a/__pycache__/PICT_numtool_ui.cpython-313.pyc b/__pycache__/PICT_numtool_ui.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e781be8743785b5699e8d7248572c3d0edc57ee4 GIT binary patch literal 29751 zcmeHQYit`wekb+v>g87=TW`~nRu(1M@=J~-%ko3AY&m+7lx#<~!=uEt#F*kL%cW!K z;)`(?T$7@`KF~WP*F(<=3|I+*hAs}ZQnW~l7HB{8LsrYf9=85apr7>Z+T7vLPw9VV zIXg>gNQykh=K$HeaAs#`elzo*|7&OdUVT|vS!RQ)`TxE*_oH(*+kc}QauvrjUmk+Y zk8K{C$NsYIvR$%YE|3Z?7fOW=TeYpp<|(MPc?xe7br%}-rQ*3FXYmHQaTe+&E_pzd z1ijcLUyKCwQkQ%+;GYxZ@T^hW__fDxzH~t5$F>_b$?ma91s=Oph~T9nPa*skdy1rz zxngIDUO5%;j`{+jtAUVTd}E^mTFn;3F;K$6e`q*g>ca?c%I+pTpr`T6WABcaI#7-H z@X1q@dI~&+-Emxp?RpV;<}M*Qnl@#4?nYxuy4;xfiG}HV*(wSdb*Nyfz+IWx1G@7R zS>|5g-jT7!uFW-oW-n|m?T#0@t5QF+^lf+V-CS>~fYz|kqk41ACRjO7agTk<7BpvR zX_2pWs^8pm`d*mp#6Wo_>tu7+ZXSOg){c#vVLhkVt@dpm;dKWob2pJ3P2DXtPjc>7 zlA|f*q!c}~G(!n(Wm)Goib`_8f6V!i_Q02>96-%>(vGG~&9sfQPtZD@v=zxU*@(PB zPbTG|C#18JE@n?0v!}yJP2o1xOykm5Lt0qql`rW#POxd3z89wSouI8r?ioqnQPLkx z^CEpLT$2ZAUsi1joWW|LHU-44IZAPyi0O<;b9hTW*%m>$|n!A@UN12)70_~ zz<%r6?b*$I>tf+Q8wTbh28pcW8Cmfd1D_&UxFdTDaFEc?f|4&DW9%oV@8vmrkDVs8p=n-tjD?$- z$5^d$u>e&m-T_$nxmbXz6c&Jm|7#kj_^@B=`Lxab9nv37^TNX{+{`@8YLyEQSEt}%7Je=kX1Aw? ztPkZ;;C2>%LKue)U)LQ*YO^uVZT;jaP2IO>p5!w2%#zO9gjS|@CzvoI_8#zeXbqA} zKr-Ls$x}cI&^*bpH3CJl$d38$LE54d5zARF$s%9;HCl({m}j|l%$~Bhil*hVj5l8z zg`o@5_wt-+lpt9Fn&w5Luy8YL6jrNTG)gL~$iipIT|MQz6#UATI|e>QvhYQSQk zSjCg1o&_aeJZ#Db>BE*w>=y%tXx*>bDlXD-lU!bSn1!2}hgq$1;o<5OR*QxIY~G7l zJ%8+LNG^k(=p*%@@4F;VQ+F<$m`ah5oh8Y6_ED;)pa+W+9T{8QBR$ZR zI5_UNXc@^t{LOx%3n4a_y)xNiHhG#8Fj<_&wo(l0CCqs162s_|B3UG+lTd=D2@-Rb zlml(HD-5$z_n3^Q4|-yx&?@APTI-+k`eW&5LDa;byk1tX8?`vs9Lq zg`dm&W_5}kO&0ziQOc5>ho_vKVhvbYtChC0aPm6@m8Mu87-NY?zFr_Tq3=H?d78Ta zl;%m!@`n5)QbtpVKs*2%u+8J_E;34)Bh8o0ebe8_STz6r|6KxmEi=m;Yc#pYT&hSS z7XDvQ-XXappOV#zB@^vTPRm{D>-v+#5A6|%u+t>)nzSCcV*ie&L0 zvR{QULUCAypOHLG-9M*!lJi(N+0ys(bzauFmi-phP z{G*g#fz$32UZQCVJ;uPNNEYsUK#@o;gZ^=mdN7_3NS>xS>7UXRyMQcw7L|_Cxn)Hq z20lfyQ0X$AAIW8)lAF|n@jN7Xn!0n*qQ_Ibak20p(Y{D72P?Kp+R`)!l#gg@k~_E! zcHbT3j(Os^MXQRC=$fUVHj8Jv6I$ITW1(pZ$zo8OB3XFGv=9_A1J7I`HDMMXlRQmx z;+YdEc!q_~qS8xrZdpkd1D_&UsPur&kK{5?$&?%z&)PmdwI^z&i|f{o8Ht1rg$r5 z;hP$V%s;_)_0m$3gT260DfBK2|5MVArk*}JK75wKYRA$Cr>R7wkRx-KhxP_F8Tb^* z!mkf#E0W6~TQ^BP3rfCZ>jc53Y5HECGuiqFI&PB7i)^uQGs_mMRW7oX%A055=OSC# z;Im|;pK^9qGQt?YCzrh;hF4AZhG0Znx4jHIX_=ed{ZE9eXbSkX^p4~#t!(bkNGVOH zEMHx^?jYsv&q1FggP=U`u8w7^nLrziwG;rGWl0}~+gk+t2 z!#{^V7^QE&BS>;!))yS{Er}6XuMGN@E_nxivM}ab5H|eKZsSGxnbbKy6vC6GjW)Q~ zw*_Z{UO(I&#dos{Gw7LKii$_YkUSsw>Z`A8dMSEA z_|&9sKeBU|~&KxSvNsHderA1+58jV6gKgcK5%cU7P1hcpUy>Fnm zkBu7m8Pdye`NX^^iyJ?|6=&yfdxz&jqk`<)2$>}lk^oKL&^ZJ%z9@t?&=I;Z505M? zZuH?>L=beVSD<=sArx7d5hT4FP5*)OF`$-(5r%GzzI2JwZA~s>6?66G8as+oW(;#uLI~^8$=xIJ6j% zfd}VB|D`}k4(mITO7z{s3-e>*xri)IErM9h`oe;a#uq0Z;(Ez}`Y3gc1VRSA_M>|= zO}F6lkB5RwQWGw5O@e&ISF#0_pmiG%gOP>MfEWR7Q#KC`1Zh|n7IaKV7_fwuNU7_h zBt{m|)RAh|IZQ-32$Jn``}_g%d_1X)^wb#McF1H(aA^TOPnmb}9e^M5@J*ZGHEw7zzOuJV8+opI<_< z4qq573JbwoQW+Y_CHM&=sc|?Q`ggmg!h#g;nhY!ld?B%GBrqfSq@}KbpcwIcWKr_X z30>nMVNeR(5e`m3RWalXb`636B65bi28BC9P+TM_tuiq@FzE$lC5s~H@rzvv_mRM% z#U;G#kwut5Rl=7F4*_9OF%s7BAz<(OwqJ3(HO`@MjtBKI?hwAKQMj7-S{@X}xR$3? zjapUvdR6_^w6aS{_Vme6Pays(hcuk170^%1^{N7k-WhR2$pH!wC(lw>yT5Nh&n--O6QJcP_OyBt(yx-U_75tBpd}*`o87|oB z=q4ZCT)C<7rxpIR%Ae7Am%_VL-V@^{f5lb&S>?Nx8rQ6F%?~cexGu8<&FkTnN3X2B zV$4nB2NZrF#toV^%r;LO4`_|Y)*FvS2cL{S9#tF9zf%Y$>=)hS$^#yJ(v`9*tg zj63zTp;c=*y54Y9Z8(l<)#6&U3RkOfZ3@@6x-Z7{U{ZCgS9e5@KRNyQv{HR8ExAUO zj?w>E(#HMDxL+H;t&HE+#>2{ZSgn$u1&MBaa3k6rl~f+h&!kRf`ZXwA!_RL0JowY# z>XB7nbeGzAEXEx-0a_KV^??kGW0ve!xcv`XA30Z?TGMf*>A2R^uQc_?xHD!Ab4j2? z;|!`|2E{B8(KTdSl@eKRsQ7Ohxe4dr-8B+DH(BG=@^f>rZv|s#dS+_Ehw%9 z%@t8x5w+^h)=7I~<%Y)hDSV&G_iOxw!cVBYJH~m?uq)n$fi@{z(*vL$2yMIsP2}Ow zqmd6rqPwGRwdEvmh4~Oy?}>2-H_sBfig8ErEZf)jwntl@9DICG*?aaIILkN*hF6BQ zmL8?0M{7B+w49G|16k0arb(-DuGcuDFFt8_+@RE)0Xl5|npgYPl~*;sU*Y>z{;bBk z72d7#lQC`z4SV~$+cnP6m@)3KS%Ng`!&{Gn9|WUEL6BOBAkpgAW89I=s|{U_f_ z>|C$uG}Mn$1M24+H%ki%POe>3TZdxY#bo*fRsve?)(Ex(3T(B60nX zj6+j&M&&_Y^`hm%<~HHsJQ(E|cQ*OqRxB^HVXOJtX05h;y_P7zTBHDXe9eOIUGZxC z8HGQi^5-<(qwpS;pNer;(8PAUyF=r!%fqvt^I4-mnp)q`NP zHqR2eigDf8O*p(>eR!?tv&v6_r-#z=G?NwHRHolltL{B3R!He5AUE){{yrvZtoorZ#q`aoDm`94ue# zb!Gf@wMu$cG~N#m15X@=P2MeWNeyB=T3A^?CZwL%>butKyQ1=wdynra^%oF9YQA29 z(fAVze?sHWDf~H=AJF(Ig`ZOSt1<2xUNrRbqH*}$ql`#_QHC7lkN131|51H(cI~L@ zjQfBZm+-je!y3)muQ>ZP=S9VNF~(i8z#7w!aUGU2+|~9N_Y#9=UQ$|Lig70yPkNMA zq}!f4oSLJ1-O;T&UP8ocz*Uitt8pC)*YQOs8YLFlgX`4?qmxf=KEA0`4F|IEQQf%Fj{lP4<#a5-&Ud_?F;PJ({b0#$#6|V6;DaJLKCFXKGt!vTh zj;z-mS$pxbhEE%mx{HWssxeKg6|UOw65;*Ru%EuQ7uLXro{MqkQ_5sMG>^u2D}1-c zpH=v?Dt}(%Cl!8D<>MMxR#Kew)Mrh)0WxbiG#=H zw3K0(d&rIIRcJ z$FdZjwI;Rgbd2k_kUhg=b5(E?>!7r@lS&&loQQeT4goLs1=ujkid(bYv&aj#vPiTQ z8X&h|ONxB6(lE8aCKAV0E4C7wURIDmBzw=Bl@4OFhbJEOtn_Hjy-IVh)_g%}zVNTN z{w?s&0d44}GIUcLx~;(fn{UU0k`|08!3b=wE!YRq<{C=yoy)kxNsFuCH#S?TyTBa5 z)@HCtxtr}RI<+a9?Ehy5N8ZYj^?IT_>#_2z!PHFf01OA=i0U*&AmOpaK^Rt?!!ho% z1p?bT(-}yVgC3e$O0k>IlNwXSgkvnES@HFJ>9cU+tazehrGA%LGSJ5 zDJ3R9TJ8GEb&Wrz@TXM%w8oDs{J6@4$KXawQ~qwb#-UBN2kpk@l2H=psJ;*SqJ>eH z+8pdFb{})%Dyz=-Jt$Rf29?`n5D&3|_oaknsl-L z3{f0|9aOn92>@=>U=%awvO6n`5+();7)ZL(jAI)U1D%2qv;6~|!z2@po0Vw!Q3GsnTgNIi}UCI&hnPP)?2w33P8NCJS{G#J~N80g$M z=}LpKgNcDonvpi@OkaB1Ba40ApF5` zCEyoC2L$Wj7!WvHcA(SMtSVaF{|azMyV*fDZ!JRQI{nCKTAaJqdf5EQ~JKs?v*@(u`>fj@}nK1h3CxMhW9)ZRgNM<+z4iLcS{mw$^`v?IY*6U?nb4cXOUitxg`WAXx zmw0ZT5EbwhR0#hkL^j>GJ*}$KsydabPOa*MQgtG}S% zYJK#M+DQB-{N{`NrYX{i63} zb^l0=8)bEQNZlXzwT;&qbNctxMtEl&GD%DxeDTsHwK2&(rwemMZHoIxNpOgozvRwH zKZHegHe$9hSP6xhSlA1P4xy+aBM1dY384rl99V;+Z6p)`g@aKf9N&ZEj2x&8voVTv zF(Qm!Ksgklg1Oq@W29~+6age5BZ8x=aQF|7-5PUl(>bp<1lq^z1xEHRi2g_r;S6s6 E|JWs(CIA2c literal 0 HcmV?d00001 diff --git a/go b/go new file mode 100755 index 0000000..51f0e46 --- /dev/null +++ b/go @@ -0,0 +1,2 @@ +python3 ./PICT_numtool.py +