def file_less_suffix(FBTS): qd = QDir( FBTS.folderpath() ) qd.setFilter(QDir.Files | QDir.Readable) if qd.exists(FBTS.basename()): a_file = QFile( qd.absoluteFilePath(FBTS.basename()) ) return _qfile_to_stream(a_file, FBTS.open_mode()) return None
def loadProfile(shortGameName, profileName, zEditInstallFolder) -> Merges: zEditProfileDir = QDir(zEditInstallFolder + "/" + ZEditConfig.RELATIVE_PROFILE_DIR) if not zEditProfileDir.exists(): moWarn("Profiles path does not exist: {}".format( zEditProfileDir.absolutePath())) return profiles = ZEditConfig.parseProfileList(shortGameName) for name in profiles: if name == profileName: relName = name + "/merges.json" if not zEditProfileDir.exists(relName): moWarn('Profile "{}" does not have a "merges.json" file.'. format(name)) return try: filePath = zEditProfileDir.absoluteFilePath(relName) with open(filePath) as f: m = Merges(json.load(f)) m.profileName = name m.profilePath = filePath return m except ValueError as ex: moWarn( 'Failed to read zEdit profile. Invalid file: "{}": {}'. format(filePath, str(ex))) moError('Profile "{}" was not found'.format(profileName)) return Merges()
def initializeProfile(self, path: QDir, settings: int): if settings & mobase.ProfileSetting.CONFIGURATION: for iniFile in self.iniFiles(): shutil.copyfile( self.documentsDirectory().absoluteFilePath(iniFile), path.absoluteFilePath(QFileInfo(iniFile).fileName()), )
def __load(self): """ Private slot to load the available scripts into the manager. """ scriptsDir = QDir(self.scriptsDirectory()) if not scriptsDir.exists(): scriptsDir.mkpath(self.scriptsDirectory()) if not scriptsDir.exists("requires"): scriptsDir.mkdir("requires") self.__disabledScripts = \ Preferences.getHelp("GreaseMonkeyDisabledScripts") from .GreaseMonkeyScript import GreaseMonkeyScript for fileName in scriptsDir.entryList(["*.js"], QDir.Files): absolutePath = scriptsDir.absoluteFilePath(fileName) script = GreaseMonkeyScript(self, absolutePath) if script.fullName() in self.__disabledScripts: script.setEnabled(False) if script.startAt() == GreaseMonkeyScript.DocumentStart: self.__startScripts.append(script) else: self.__endScripts.append(script)
def file_less_suffix(FBTS): qd = QDir(FBTS.folderpath()) qd.setFilter(QDir.Files | QDir.Readable) if qd.exists(FBTS.basename()): a_file = QFile(qd.absoluteFilePath(FBTS.basename())) return _qfile_to_stream(a_file, FBTS.open_mode()) return None
def pluginToQDateTime(plugin): lhm = self.__organizer.getMod(pluginList.origin(plugin)) lhd = self.__organizer.managedGame().dataDirectory() if lhm != None: lhd = QDir(lhm.absolutePath()) lhp = lhd.absoluteFilePath(plugin) return QFileInfo(lhp).lastModified()
def __load(self): """ Private slot to load the available scripts into the manager. """ scriptsDir = QDir(self.scriptsDirectory()) if not scriptsDir.exists(): scriptsDir.mkpath(self.scriptsDirectory()) if not scriptsDir.exists("requires"): scriptsDir.mkdir("requires") self.__disabledScripts = Preferences.getWebBrowser( "GreaseMonkeyDisabledScripts") from .GreaseMonkeyScript import GreaseMonkeyScript for fileName in scriptsDir.entryList(["*.js"], QDir.Files): absolutePath = scriptsDir.absoluteFilePath(fileName) script = GreaseMonkeyScript(self, absolutePath) if not script.isValid(): del script continue self.__scripts.append(script) if script.fullName() in self.__disabledScripts: script.setEnabled(False) else: collection = WebBrowserWindow.webProfile().scripts() collection.insert(script.webScript()) self.__jsObject.setSettingsFile( os.path.join(Utilities.getConfigDir(), "web_browser", "greasemonkey_values.ini")) ExternalJsObject.registerExtraObject("GreaseMonkey", self.__jsObject)
def getProfiles(shortGameName, zEditInstallFolder) -> List[Merges]: """ This returns the content of each profiles 'merges.json' """ result = [] zEditProfileDir = QDir(zEditInstallFolder + "/" + ZEditConfig.RELATIVE_PROFILE_DIR) if not zEditProfileDir.exists(): qDebug("Profiles path does not exist: {}".format( zEditProfileDir.absolutePath())) return result profiles = ZEditConfig.parseProfileList(shortGameName) for name in profiles: relName = name + "/merges.json" if not zEditProfileDir.exists(relName): continue try: filePath = zEditProfileDir.absoluteFilePath(relName) with open(filePath) as f: m = Merges(json.load(f)) m.profileName = name m.profilePath = filePath result.append(m) except ValueError as ex: qWarning('Invalid file "{}": {}'.format(filePath, str(ex))) return result
def parseProfileList(shortGameName: str): # the profile list is stored in Roaming/zEdit/profiles.json result = [] gameMode = ZEditConfig.getZEditGameMode(shortGameName) if gameMode is None: qWarning("Game type is not supported by zMerge.") return result appData = QDir( QStandardPaths.writableLocation(QStandardPaths.AppDataLocation)) profilesPath = appData.absoluteFilePath( ZEditConfig.RELATIVE_ZEDIT_PROFILES_FILE) if not appData.exists(ZEditConfig.RELATIVE_ZEDIT_PROFILES_FILE): qDebug('"{}" does not exist'.format(profilesPath)) return result try: file = open(profilesPath, "r", encoding="utf8") profiles = json.load(file) for profile in profiles: if profile["gameMode"] == gameMode: result.append(profile["name"]) except OSError: qWarning('Failed to read "profiles.json".') return [] except (TypeError, ValueError): qWarning('"profiles.json" has unknown file structure.') return result
def __init__(self, parent=None): super(MainApp, self).__init__(parent) self.setupUi(self) #init statusbar self.setStyleSheet("QStatusBar::item { border: 0px solid black }; ") self.statusBar = QLabel() self.statusbar.addWidget(self.statusBar) self.statusBar.setText("No configuration file loaded.") #object variables self.currentPlugin = False self.pluginContainer = None self.selectedElement = None self.unsavedContent = False self.editorContainer = None self.ignoreWidgetsToSettingsData = False self.fileHandler = None self.lastFile = "" #buttons self.button_add.setEnabled(False) self.button_del.setEnabled(False) #signals self.actionLoad_Configuration.triggered.connect(self.loadFileDialog) self.actionSave_Configuration.triggered.connect(self.saveFile) self.actionSave_Configuration_File_As.triggered.connect(self.saveFileAs) self.actionNew_Configuration_File.triggered.connect(self.newFile) self.actionExit.triggered.connect(self.closeEventButton) self.actionAbout_This_Program.triggered.connect(self.showAboutWidget) self.button_add.clicked.connect(self.showAddSettingWidget) self.button_del.clicked.connect(self.deleteSetting) #treemodel self.treeView.setContextMenuPolicy(Qt.CustomContextMenu) self.treeView.customContextMenuRequested.connect(self.treeViewOpenMenu) self.treeView.mousePressEvent = MethodType(mousePressEvent, self.treeView) self.activeModel = SettingTree(self) self.proxyModel = FilterProxyModel(self) self.proxyModel.setSourceModel(self.activeModel) self.treeView.setModel(self.proxyModel) self.treeView.setColumnHidden(1, True) self.completeModel = SettingTree(HeaderData(True), self) #plugin content self.contentXML = None #icon self.icon = "" dataDir = QDir(os.path.dirname(os.path.realpath(__file__))) dataDir.cd("data") if dataDir.exists("icon.png"): self.icon = dataDir.absoluteFilePath("icon.png") self.setWindowIcon(QIcon(self.icon)) #programm configuration for window size and last filename self.programConfig = ProgramConfiguration(self) self.programConfig.loadConfiguration()
def findQmFiles(self): trans_dir = QDir('./translations/') fileNames = trans_dir.entryList(['*.qm'], QDir.Files, QDir.Name) trans = {} for i in fileNames: trans[QFileInfo(i).baseName()] = trans_dir.absoluteFilePath(i) return trans
def __init__(self, parent): QDialog.__init__(self, parent) # Set up the user interface from Designer. self.ui = ui = Ui_SettingsDialog() ui.setupUi(self) ui.lineEdit_BrowserPath.setPlaceholderText( "Leave this empty to use your default browser") ui.pushButton_Browse.clicked.connect(self.browseClicked) # load settings settings = QSettings() ui.lineEdit_BrowserPath.setText( settings.value("/Qgis2threejs/browser", "", type=str)) enabled_plugins = QSettings().value("/Qgis2threejs/plugins", "", type=str).split(",") # initialize plugin table widget plugin_dir = QDir(pluginDir("plugins")) plugins = plugin_dir.entryList(QDir.Dirs | QDir.NoSymLinks | QDir.NoDotAndDotDot) tableWidget = ui.tableWidget_Plugins tableWidget.setColumnCount(1) tableWidget.setHorizontalHeaderLabels(["Name"]) tableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) headerView = tableWidget.horizontalHeader() headerView.setSectionResizeMode(QHeaderView.Stretch) self.plugin_metadata = [] for i, name in enumerate(plugins): if name[0] == "_": # skip __pycache__ dir. continue parser = configparser.SafeConfigParser() try: with open(os.path.join(plugin_dir.absoluteFilePath(name), "metadata.txt"), "r", encoding="UTF-8") as f: parser.readfp(f) metadata = dict(parser.items("general")) self.plugin_metadata.append(metadata) except Exception as e: logMessage("Unable to read metadata of plugin: {} ({})".format( name, e)) tableWidget.setRowCount(len(self.plugin_metadata)) for i, metadata in enumerate(self.plugin_metadata): item = QTableWidgetItem(metadata.get("name", name)) item.setCheckState(Qt.Checked if name in enabled_plugins else Qt.Unchecked) tableWidget.setItem(i, 0, item) tableWidget.selectionModel().currentRowChanged.connect( self.pluginSelectionChanged)
def related_file(FBTS, filename, encoding=None): qd = QDir(FBTS.folderpath()) qd.setFilter(QDir.Files | QDir.Readable) qd.setSorting(QDir.Type | QDir.Reversed) qd.setNameFilters([filename]) # literal name or 'foo*.*' names = qd.entryList() if names: # list is not empty, open the first a_file = QFile(qd.absoluteFilePath(names[0])) return _qfile_to_stream(a_file, QIODevice.ReadOnly, encoding) return None
def related_file(FBTS, filename, encoding=None): qd = QDir( FBTS.folderpath() ) qd.setFilter(QDir.Files | QDir.Readable) qd.setSorting(QDir.Type | QDir.Reversed) qd.setNameFilters( [filename] ) # literal name or 'foo*.*' names = qd.entryList() if names : # list is not empty, open the first a_file = QFile( qd.absoluteFilePath(names[0]) ) return _qfile_to_stream(a_file, QIODevice.ReadOnly, encoding) return None
def tempDirCreate(self, basedir, name=None): tmpdir = QDir(basedir) if not tmpdir.exists(): return uid = name if name else str(uuid.uuid4()) path = tmpdir.absoluteFilePath(uid) if tmpdir.mkpath(path): return path
def on_btnDir_absFilePath_clicked(self): self.__showBtnInfo(self.sender()) sous = self.ui.editFile.text() if sous == "": self.ui.textEdit.appendPlainText("请先选择一个文件") return parDir = QDir.currentPath() dirObj = QDir(parDir) text = dirObj.absoluteFilePath(sous) self.ui.textEdit.appendPlainText(text + "\n")
def WriteToFootprint(name: str, path: str, polygons, createNew: bool = True): header = \ "(module {0} (layer F.Cu,) (tedit 5F08A9C4)\n".format(name) + \ " (fp_text reference REF** (at -0.03 -2.63) (layer F.SilkS)\n" + \ " (effects (font (size 1 1) (thickness 0.15)))\n" + \ " )\n" + \ " (fp_text value {} (at 0.08 -4.13) (layer F.Fab) hide\n".format(name) + \ " (effects (font (size 1 1) (thickness 0.15)))\n" + \ " )\n" path = "/home/dom/kicad/FP/test.pretty/" dir = QDir(path) filePath = dir.absoluteFilePath(name + ".kicad_mod") if createNew: # delete it since we are creating it new if (QFile(filePath).exists()): LogInfo("Deleting existing footprint at: {}".format(filePath)) os.remove(filePath) fileFP = open(filePath,'w') fileFP.write(header) # no iterate through the polygon and write # pcbnew.DRAWSEGMENT.GetPolyShape(). # pcbnew.DRAWSEGMENT.BuildPolyPointsList() # (fp_poly (pts (xy 18.79 0.499) (xy 17.74 0.499) (xy 17.74 -0.501) (xy 18.79 -0.501)) (layer F.Mask) (width 0)) for polygon in polygons: points = polygon.BuildPolyPointsList() pcbnew.DRAWSEGMENT points_str = "" for point in points: points_str += " (xy {:.3f} {:.3f})".format(pcbnew.ToMM(point.x),pcbnew.ToMM(point.y)) fileFP.write("(fp_poly (pts {}) (layer {}) (width {}))".format(points_str, pcbnew.BOARD_GetStandardLayerName(polygon.GetLayer()), polygon.GetWidth())) # write the final closing ')' fileFP.write("\n)") fileFP.close()
def main(): facesDir = QDir("/home/syndicate/PycharmProjects/Iris/assets/faces/") faceFolders = facesDir.entryList() normalizer = FaceNormalizer() for folder in faceFolders: temp = QDir("/home/syndicate/PycharmProjects/Iris/assets/faces/" + folder) temp.setNameFilters(["*.png"]) images = temp.entryList() for image in images: path = temp.absoluteFilePath(image) image = cv2.imread(path, 0) cv2.imshow("image", image) print(path) frame = Face(image) normalized = frame.normalize() cv2.imwrite(path, normalized) if cv2.waitKey(10) & 0xFF == ord('q'): cv2.destroyAllWindows() exit(1)
class Window(QDialog): def __init__(self, parent=None): super(Window, self).__init__(parent) if getattr(sys, 'frozen', False): bundle_dir = os.path.abspath(sys.executable + "/../../../") else: bundle_dir = os.path.os.path.abspath(__file__) self.workingDir = os.path.dirname(bundle_dir) # print(self.workingDir) # print(os.path.abspath(bundle_dir + "/../../")) # Initialize user settings self.searchRecursivity = QDirIterator.Subdirectories self.countRecursivity = QDirIterator.NoIteratorFlags self.dirFilters = (QDir.Dirs | QDir.NoDotAndDotDot) # to swap size and count in file view self.dirView = True self.subfolderLevel = 0 # Row 0 directoryLabel = QLabel("In folder:") self.directoryComboBox = self.createComboBox(self.workingDir) browseButton = self.createButton("&Browse...", self.browse) # Row 1 filterLabel = QLabel("Filter:") self.filterComboBox = self.createComboBox('*') self.typeComboBox = QComboBox() self.typeComboBox.addItem("All") self.typeComboBox.addItem("Folders") self.typeComboBox.addItem("Files") self.typeComboBox.setCurrentIndex(1) self.typeComboBox.currentIndexChanged.connect(self.changeType) self.checkBox = QCheckBox("Subfolders") self.checkBox.toggle() self.checkBox.stateChanged.connect(self.changeSearchRecursivity) self.findButton = self.createButton("&Find", self.find) # Row 2 self.depthLabel = QLabel("Count files in subfolder level") self.folderDepthSpinBox = QSpinBox() self.folderDepthSpinBox.valueChanged.connect(self.changeFolderDepth) self.checkBox_2 = QCheckBox("Count all files") self.checkBox_2.stateChanged.connect(self.changeCountRecursivity) # Row 3 Table self.createFilesTable() # Row 5 self.filesFoundLabel = QLabel() saveButton = self.createButton("&Save", self.saveSheet) # Not implemented, for full text search self.textComboBox = self.createComboBox() textLabel = QLabel("Containing text:") mainLayout = QGridLayout() mainLayout.addWidget(directoryLabel, 0, 0, 1, 1) mainLayout.addWidget(self.directoryComboBox, 0, 1, 1, 5) # Row 1 mainLayout.addWidget(filterLabel, 1, 0, 1, 1) mainLayout.addWidget(self.filterComboBox, 1, 1, 1, 5) mainLayout.addWidget(self.typeComboBox, 1, 6, 1, 1) mainLayout.addWidget(self.findButton, 1, 7, 1, 1) # Row 0 mainLayout.addWidget(self.checkBox, 0, 6, 1, 1) mainLayout.addWidget(browseButton, 0, 7, 1, 1) # Row 2 mainLayout.addWidget(self.depthLabel, 2, 4, 1, 1, Qt.AlignRight) mainLayout.addWidget(self.folderDepthSpinBox, 2, 5, 1, 1) mainLayout.addWidget(self.checkBox_2, 2, 6, 1, 2) # Table mainLayout.addWidget(self.filesTable, 4, 0, 1, 8) # Row 5 mainLayout.addWidget(self.filesFoundLabel, 5, 0, 1, 4) mainLayout.addWidget(saveButton, 5, 7, 1, 1) self.setLayout(mainLayout) self.setWindowTitle("bdrc-audit 0.1") self.resize(520, 440) # self.resize(442, 440) def changeCountRecursivity(self, state): if state == Qt.Checked: self.countRecursivity = QDirIterator.Subdirectories else: self.countRecursivity = QDirIterator.NoIteratorFlags def changeFolderDepth(self, value): self.subfolderLevel = value def changeType(self, typeIndex): if typeIndex == 0: self.dirFilters = (QDir.AllEntries | QDir.NoSymLinks | QDir.NoDotAndDotDot) self.dirView = False self.filesTable.setHorizontalHeaderLabels(("Item Path", "Size")) self.subfolderLevel = 0 self.depthLabel.hide() self.folderDepthSpinBox.hide() elif typeIndex == 1: self.dirFilters = (QDir.Dirs | QDir.NoDotAndDotDot) self.dirView = True self.filesTable.setHorizontalHeaderLabels( ("Folder Path", "File Count")) self.depthLabel.show() self.folderDepthSpinBox.show() elif typeIndex == 2: self.dirFilters = (QDir.Files | QDir.NoSymLinks) self.dirView = False self.filesTable.setHorizontalHeaderLabels(("File Path", "Size")) self.subfolderLevel = 0 self.depthLabel.hide() self.folderDepthSpinBox.hide() def changeSearchRecursivity(self, state): if state == Qt.Checked: self.searchRecursivity = QDirIterator.Subdirectories else: self.searchRecursivity = QDirIterator.NoIteratorFlags def browse(self): directory = QFileDialog.getExistingDirectory(self, "Find files", self.workingDir) if directory: if self.directoryComboBox.findText(directory) == -1: self.directoryComboBox.addItem(directory) self.directoryComboBox.setCurrentIndex( self.directoryComboBox.findText(directory)) @staticmethod def updateComboBox(comboBox): if comboBox.findText(comboBox.currentText()) == -1: comboBox.addItem(comboBox.currentText()) def find(self): self.filesTable.setRowCount(0) self.path = self.directoryComboBox.currentText() fileName = self.filterComboBox.currentText() files = [] if not fileName: fileName = "*" fileName = fileName.split(", ") # not used, kept for full text search text = self.textComboBox.currentText() self.updateComboBox(self.directoryComboBox) self.updateComboBox(self.filterComboBox) self.updateComboBox(self.textComboBox) self.currentDir = QDir(self.path) self.it = QDirIterator(self.path, fileName, self.dirFilters, self.searchRecursivity) while self.it.hasNext(): files.append(self.it.next()) # For full text search, not used if text: files = self.findFiles(files, text) self.showFiles(files) # For full text search, not used def findFiles(self, files, text): progressDialog = QProgressDialog(self) progressDialog.setCancelButtonText("&Cancel") progressDialog.setRange(0, files.count()) progressDialog.setWindowTitle("Find Files") foundFiles = [] for i in range(files.count()): progressDialog.setValue(i) progressDialog.setLabelText("Searching file number %d of %d..." % (i, files.count())) QApplication.processEvents() if progressDialog.wasCanceled(): break inFile = QFile(self.currentDir.absoluteFilePath(files[i])) if inFile.open(QIODevice.ReadOnly): stream = QTextStream(inFile) while not stream.atEnd(): if progressDialog.wasCanceled(): break line = stream.readLine() if text in line: foundFiles.append(files[i]) break progressDialog.close() return foundFiles def showFiles(self, files): for fn in files: # file = QFile(self.currentDir.relativeFilePath(fn)) # May change in countfile() self.pathToDisplay = fn # print(QFileInfo(file).baseName) if os.path.isdir(fn): size = count.countFiles(self, fn) if self.dirView: sizeItem = QTableWidgetItem("%d" % size) else: sizeItem = QTableWidgetItem("%d files" % size) else: size = QFileInfo(fn).size() sizeItem = QTableWidgetItem("%d KB" % (int( (size + 1023) / 1024))) head = '.' if self.path.endswith('/'): head = './' fileNameItem = QTableWidgetItem( self.pathToDisplay.replace(self.path, head)) fileNameItem.setFlags(fileNameItem.flags() ^ Qt.ItemIsEditable) sizeItem.setTextAlignment(Qt.AlignVCenter | Qt.AlignRight) sizeItem.setFlags(sizeItem.flags() ^ Qt.ItemIsEditable) row = self.filesTable.rowCount() self.filesTable.insertRow(row) self.filesTable.setItem(row, 0, fileNameItem) self.filesTable.setItem(row, 1, sizeItem) # if self.pathToDisplay == fn and size == 0: # self.filesTable.item(row, 0).setBackground(QColor(211,211,211)) self.filesFoundLabel.setText("%d matches. Double click to open." % len(files)) def createButton(self, text, member): button = QPushButton(text) button.clicked.connect(member) return button def createComboBox(self, text=""): comboBox = QComboBox() comboBox.setEditable(True) comboBox.addItem(text) comboBox.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) return comboBox def createFilesTable(self): self.filesTable = QTableWidget(0, 2) self.filesTable.setSelectionBehavior(QAbstractItemView.SelectRows) self.filesTable.setHorizontalHeaderLabels( ("Folder Path", "File Count")) self.filesTable.horizontalHeader().setSectionResizeMode( 0, QHeaderView.Stretch) self.filesTable.verticalHeader().hide() self.filesTable.setShowGrid(False) self.filesTable.setSortingEnabled(True) self.filesTable.sortByColumn(0, Qt.AscendingOrder) self.filesTable.cellActivated.connect(self.openFileOfItem) def saveSheet(self): import os import csv path = QFileDialog.getSaveFileName(self, 'Save CSV', self.workingDir, 'CSV(*.csv)') if path[0] != '': with open(path[0], 'w') as csv_file: writer = csv.writer(csv_file, dialect='excel') for row in range(self.filesTable.rowCount()): row_data = [] for column in range(self.filesTable.columnCount()): item = self.filesTable.item(row, column) if item is not None: row_data.append(item.text()) else: row_data.append('') row_data[0] = self.path + row_data[0][1:] writer.writerow(row_data) QMessageBox.information( self, "Export Successful", "Your search result has been exported as a csv.") def openFileOfItem(self, row, column): item = self.filesTable.item(row, 0) # Complete links to make them click tail = item.text()[1:] if self.path.endswith('/'): tail = item.text()[2:] path = self.path + tail print(path) QDesktopServices.openUrl( QUrl.fromLocalFile(self.currentDir.absoluteFilePath(path)))
class Window(QDialog): def __init__(self, parent=None): super(Window, self).__init__(parent) browseButton = self.createButton("&Browse...", self.browse) findButton = self.createButton("&Find", self.find) self.fileComboBox = self.createComboBox("*") self.textComboBox = self.createComboBox() self.directoryComboBox = self.createComboBox(QDir.currentPath()) fileLabel = QLabel("Named:") textLabel = QLabel("Containing text:") directoryLabel = QLabel("In directory:") self.filesFoundLabel = QLabel() self.createFilesTable() buttonsLayout = QHBoxLayout() buttonsLayout.addStretch() buttonsLayout.addWidget(findButton) mainLayout = QGridLayout() mainLayout.addWidget(fileLabel, 0, 0) mainLayout.addWidget(self.fileComboBox, 0, 1, 1, 2) mainLayout.addWidget(textLabel, 1, 0) mainLayout.addWidget(self.textComboBox, 1, 1, 1, 2) mainLayout.addWidget(directoryLabel, 2, 0) mainLayout.addWidget(self.directoryComboBox, 2, 1) mainLayout.addWidget(browseButton, 2, 2) mainLayout.addWidget(self.filesTable, 3, 0, 1, 3) mainLayout.addWidget(self.filesFoundLabel, 4, 0) mainLayout.addLayout(buttonsLayout, 5, 0, 1, 3) self.setLayout(mainLayout) self.setWindowTitle("Find Files") self.resize(700, 300) def browse(self): directory = QFileDialog.getExistingDirectory(self, "Find Files", QDir.currentPath()) if directory: if self.directoryComboBox.findText(directory) == -1: self.directoryComboBox.addItem(directory) self.directoryComboBox.setCurrentIndex(self.directoryComboBox.findText(directory)) @staticmethod def updateComboBox(comboBox): if comboBox.findText(comboBox.currentText()) == -1: comboBox.addItem(comboBox.currentText()) def find(self): self.filesTable.setRowCount(0) fileName = self.fileComboBox.currentText() text = self.textComboBox.currentText() path = self.directoryComboBox.currentText() self.updateComboBox(self.fileComboBox) self.updateComboBox(self.textComboBox) self.updateComboBox(self.directoryComboBox) self.currentDir = QDir(path) if not fileName: fileName = "*" files = self.currentDir.entryList([fileName], QDir.Files | QDir.NoSymLinks) if text: files = self.findFiles(files, text) self.showFiles(files) def findFiles(self, files, text): progressDialog = QProgressDialog(self) progressDialog.setCancelButtonText("&Cancel") progressDialog.setRange(0, files.count()) progressDialog.setWindowTitle("Find Files") foundFiles = [] for i in range(files.count()): progressDialog.setValue(i) progressDialog.setLabelText("Searching file number %d of %d..." % (i, files.count())) QApplication.processEvents() if progressDialog.wasCanceled(): break inFile = QFile(self.currentDir.absoluteFilePath(files[i])) if inFile.open(QIODevice.ReadOnly): stream = QTextStream(inFile) while not stream.atEnd(): if progressDialog.wasCanceled(): break line = stream.readLine() if text in line: foundFiles.append(files[i]) break progressDialog.close() return foundFiles def showFiles(self, files): for fn in files: file = QFile(self.currentDir.absoluteFilePath(fn)) size = QFileInfo(file).size() fileNameItem = QTableWidgetItem(fn) fileNameItem.setFlags(fileNameItem.flags() ^ Qt.ItemIsEditable) sizeItem = QTableWidgetItem("%d KB" % (int((size + 1023) / 1024))) sizeItem.setTextAlignment(Qt.AlignVCenter | Qt.AlignRight) sizeItem.setFlags(sizeItem.flags() ^ Qt.ItemIsEditable) row = self.filesTable.rowCount() self.filesTable.insertRow(row) self.filesTable.setItem(row, 0, fileNameItem) self.filesTable.setItem(row, 1, sizeItem) self.filesFoundLabel.setText("%d file(s) found (Double click on a file to open it)" % len(files)) def createButton(self, text, member): button = QPushButton(text) button.clicked.connect(member) return button def createComboBox(self, text=""): comboBox = QComboBox() comboBox.setEditable(True) comboBox.addItem(text) comboBox.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) return comboBox def createFilesTable(self): self.filesTable = QTableWidget(0, 2) self.filesTable.setSelectionBehavior(QAbstractItemView.SelectRows) self.filesTable.setHorizontalHeaderLabels(("File Name", "Size")) self.filesTable.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) self.filesTable.verticalHeader().hide() self.filesTable.setShowGrid(False) self.filesTable.cellActivated.connect(self.openFileOfItem) def openFileOfItem(self, row, column): item = self.filesTable.item(row, 0) QDesktopServices.openUrl(QUrl(self.currentDir.absoluteFilePath(item.text())))
class Window(QDialog): def __init__(self, parent=None): super(Window, self).__init__(parent) browseButton = self.createButton("&Browse...", self.browse) self.findButton = self.createButton("&Find", self.find) self.fileComboBox = self.createComboBox("*") self.textComboBox = self.createComboBox() self.directoryComboBox = self.createComboBox(QDir.currentPath()) fileLabel = QLabel("Named:") textLabel = QLabel("Containing text:") directoryLabel = QLabel("In directory:") self.filesFoundLabel = QLabel() self.createFilesTable() buttonsLayout = QHBoxLayout() buttonsLayout.addStretch() buttonsLayout.addWidget(self.findButton) mainLayout = QGridLayout() mainLayout.addWidget(fileLabel, 0, 0) mainLayout.addWidget(self.fileComboBox, 0, 1, 1, 2) mainLayout.addWidget(textLabel, 1, 0) mainLayout.addWidget(self.textComboBox, 1, 1, 1, 2) mainLayout.addWidget(directoryLabel, 2, 0) mainLayout.addWidget(self.directoryComboBox, 2, 1) mainLayout.addWidget(browseButton, 2, 2) mainLayout.addWidget(self.filesTable, 3, 0, 1, 3) mainLayout.addWidget(self.filesFoundLabel, 4, 0) mainLayout.addLayout(buttonsLayout, 5, 0, 1, 3) self.setLayout(mainLayout) self.setWindowTitle("Find Files") self.resize(700, 300) def browse(self): directory = QFileDialog.getExistingDirectory(self, "Find Files", QDir.currentPath()) if directory: if self.directoryComboBox.findText(directory) == -1: self.directoryComboBox.addItem(directory) self.directoryComboBox.setCurrentIndex(self.directoryComboBox.findText(directory)) @staticmethod def updateComboBox(comboBox): if comboBox.findText(comboBox.currentText()) == -1: comboBox.addItem(comboBox.currentText()) def find(self): self.filesTable.setRowCount(0) fileName = self.fileComboBox.currentText() text = self.textComboBox.currentText() path = self.directoryComboBox.currentText() self.updateComboBox(self.fileComboBox) self.updateComboBox(self.textComboBox) self.updateComboBox(self.directoryComboBox) self.currentDir = QDir(path) if not fileName: fileName = "*" files = self.currentDir.entryList([fileName], QDir.Files | QDir.NoSymLinks) if text: files = self.findFiles(files, text) self.showFiles(files) def findFiles(self, files, text): progressDialog = QProgressDialog(self) progressDialog.setCancelButtonText("&Cancel") progressDialog.setRange(0, files.count()) progressDialog.setWindowTitle("Find Files") foundFiles = [] for i in range(files.count()): progressDialog.setValue(i) progressDialog.setLabelText("Searching file number %d of %d..." % (i, files.count())) QApplication.processEvents() if progressDialog.wasCanceled(): break inFile = QFile(self.currentDir.absoluteFilePath(files[i])) if inFile.open(QIODevice.ReadOnly): stream = QTextStream(inFile) while not stream.atEnd(): if progressDialog.wasCanceled(): break line = stream.readLine() if text in line: foundFiles.append(files[i]) break progressDialog.close() return foundFiles def showFiles(self, files): for fn in files: file = QFile(self.currentDir.absoluteFilePath(fn)) size = QFileInfo(file).size() fileNameItem = QTableWidgetItem(fn) fileNameItem.setFlags(fileNameItem.flags() ^ Qt.ItemIsEditable) sizeItem = QTableWidgetItem("%d KB" % (int((size + 1023) / 1024))) sizeItem.setTextAlignment(Qt.AlignVCenter | Qt.AlignRight) sizeItem.setFlags(sizeItem.flags() ^ Qt.ItemIsEditable) row = self.filesTable.rowCount() self.filesTable.insertRow(row) self.filesTable.setItem(row, 0, fileNameItem) self.filesTable.setItem(row, 1, sizeItem) self.filesFoundLabel.setText("%d file(s) found (Double click on a file to open it)" % len(files)) def createButton(self, text, member): button = QPushButton(text) button.clicked.connect(member) return button def createComboBox(self, text=""): comboBox = QComboBox() comboBox.setEditable(True) comboBox.addItem(text) comboBox.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) return comboBox def createFilesTable(self): self.filesTable = QTableWidget(0, 2) self.filesTable.setSelectionBehavior(QAbstractItemView.SelectRows) self.filesTable.setHorizontalHeaderLabels(("File Name", "Size")) self.filesTable.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) self.filesTable.verticalHeader().hide() self.filesTable.setShowGrid(False) self.filesTable.cellActivated.connect(self.openFileOfItem) def openFileOfItem(self, row, column): item = self.filesTable.item(row, 0) QDesktopServices.openUrl(QUrl(self.currentDir.absoluteFilePath(item.text())))
def set_path(self,book_folder_path): book_dir = QDir(book_folder_path) if book_dir.exists('pngs') : self.png_dir = QDir(book_dir.absoluteFilePath('pngs')) self._enable() self.cursor_move()
class ImageDisplay(QWidget): def __init__(self, my_book, parent=None): super().__init__(parent) self.my_book = my_book # register metadata readers and writers md = my_book.get_meta_manager() md.register(C.MD_IZ,self._zoom_read,self._zoom_write) md.register(C.MD_IX,self._link_read,self._link_write) # Create our widgets including cursor_to_image and # image_to_cursor pushbuttons. self._uic() # set defaults in case no metadata self.cursor_to_image.setChecked(True) self.image_to_cursor.setChecked(False) self.zoom_factor = 0.25 self.png_path = None # disable all widgetry until we get some metadata self._disable() # end of __init__() # Disable our widgets because we have no image to show. def _disable(self): self.no_image = True self.last_index = None # compares unequal to any self.pix_map = QPixmap() self.image = QImage() self.cursor_to_image.setEnabled(False) self.image_to_cursor.setEnabled(False) self.zoom_pct.setEnabled(False) self.zoom_to_width.setEnabled(False) self.zoom_to_height.setEnabled(False) self.image_display.setPixmap(self.gray_image) self.image_display.setToolTip( _TR('Image view tooltip', 'Display of one scanned page (no images available)') ) # Enable our widgets, we have images to show. At this time the Book # has definitely created an edit view and a page model. def _enable(self): self.edit_view = self.my_book.get_edit_view() self.editor = self.edit_view.Editor # access to actual QTextEdit self.page_data = self.my_book.get_page_model() self.cursor_to_image.setEnabled(True) self.image_to_cursor.setEnabled(True) self.zoom_to_width.setEnabled(True) self.zoom_to_height.setEnabled(True) self.image_display.setToolTip( _TR('Image view tooltip', 'Display of one scanned page from the book') ) self.no_image = False self.zoom_pct.setEnabled(True) # the following triggers entry to _new_zoom_pct() below self.zoom_pct.setValue(int(100*self.zoom_factor)) # Metadata: read or write the {{IMAGEZOOM f}} section. # Parameter f should be a decimal number between 0.15 and 2.0 # but we do not depend on text the user could edit. def _zoom_read(self, qts, section, vers, parm): try: z = float(parm) # throws exception on a bad literal if math.isnan(z) or (z < 0.15) or (z > 2.0) : raise ValueError self.zoom_factor = z except: imageview_logger.error('Invalid IMAGEZOOM "{0}" ignored'.format(parm)) def _zoom_write(self, qts, section): qts << metadata.open_line(section, str(self.zoom_factor)) # Metadata: read or write the {{IMAGELINK b}} section. The parameter should # be an int 0/1/2/3. Bit 0 represents the state of cursor_to_image # (usually 1); bit 1 represents the state of image_to_cursor (usually 0). def _link_read(self, qts, section, vers, parm): try: b = int(parm) # exception on a bad literal if (b < 0) or (b > 3) : raise ValueError self.cursor_to_image.setChecked( True if b & 1 else False ) self.image_to_cursor.setChecked( True if b & 2 else False ) except : imageview_logger.error('Invalid IMAGELINKING "{0}" ignored'.format(parm)) def _link_write(self, qts, section): b = 0 if self.cursor_to_image.isChecked() : b |= 1 if self.image_to_cursor.isChecked() : b |= 2 qts << metadata.open_line(section, str(b)) # The Book calls here after it has loaded a book with defined page data, # passing the path to the folder containing the book. If we can find a # folder named 'pngs' in it we record that path and enable our widgets, # and fake a cursorMoved signal to display the current edit page. def set_path(self,book_folder_path): book_dir = QDir(book_folder_path) if book_dir.exists('pngs') : self.png_dir = QDir(book_dir.absoluteFilePath('pngs')) self._enable() self.cursor_move() # Come here to display or re-display an image. The last-displayed # page image index (if any) is in self.last_index. The desired page # index is passed as the argument, which may be: # * the same as last_index, for example on a change of zoom%. Just # redisplay the current page. # * negative or None if the cursor is "above" the first available page or on # a Page-Up keystroke. Display the gray image. # * greater than page_data.page_count() on a Page-Down keystroke, # display the last available page. # If different from last_index, try to load the .png file for that # page. If that fails, use the gray image. Otherwise display that # page and save it as last_index. def _show_page(self, page_index): if page_index != self.last_index : self.last_index = page_index # change of page, see if we have a filename for it self.pix_map = self.gray_image # assume failure... im_name = self.page_data.filename(page_index) if im_name : # pagedata has a filename; of course there is no guarantee # such a file exists now or ever did. im_name += '.png' if self.png_dir.exists(im_name) : self.image = QImage(self.png_dir.absoluteFilePath(im_name)) if not self.image.isNull(): # we loaded it ok, make a full-scale pixmap for display self.pix_map = QPixmap.fromImage(self.image,Qt.ColorOnly) # Whether new page or not, rescale to current zoom. The .resize method # takes a QSize; pix_map.size() returns one, and it supports * by a real. self.image_display.setPixmap(self.pix_map) self.image_display.resize( self.zoom_factor * self.pix_map.size() ) # Slot to receive the cursorMoved signal from the editview widget. If we # are in no_image state, do nothing. If the cursor_to_image switch is # not checked, do nothing. Else get the character position of # the high-end of the current edit selection, and use that to get the # current page index from pagedata, and pass that to _show_page. def cursor_move(self): if self.no_image : return if self.cursor_to_image.isChecked() : pos = self.editor.textCursor().selectionEnd() self._show_page( self.page_data.page_index(pos) ) # Slots to receive the signals from our zoom percent and zoom-to buttons. # The controls are disabled while we are in no_image state, so if a signal # arrives, we are not in that state. # # These are strictly internal hence _names. # Any change in the value of the zoom % spin-box including setValue(). def _new_zoom_pct(self,new_value): self.zoom_factor = self.zoom_pct.value() / 100 self._show_page(self.last_index) # Set a new zoom factor (a real) and update the zoom pct spinbox. # Setting zoom_pct triggers a signal to _new_zoom_pct above, and # thence to _show_page which repaints the page at the new scale value. def _set_zoom_real(self,new_value): zoom = max(new_value, ZOOM_FACTOR_MIN) zoom = min(zoom, ZOOM_FACTOR_MAX) self.zoom_factor = zoom self.zoom_pct.setValue(int(100*zoom)) # Re-implement keyPressEvent in order to provide zoom and page up/down. # ctrl-plus increases the image size by 1.25 # ctrl-minus decreases the image size by 0.8 # page-up displays the next-higher page # page-down displays the next-lower page def keyPressEvent(self, event): # assume we will not handle this key and clear its accepted flag event.ignore() if self.no_image or (self.last_index is None) : return # ignore keys until we are showing some image # We have images to show, check the key value. modkey = int( int(event.key() | (int(event.modifiers()) & C.KEYPAD_MOD_CLEAR)) ) if modkey in C.KEYS_ZOOM : event.accept() fac = (0.8) if (modkey == C.CTL_MINUS) else (1.25) self._set_zoom_real( fac * self.zoom_factor) elif (event.key() == Qt.Key_PageUp) or (event.key() == Qt.Key_PageDown) : event.accept() pgix = self.last_index + (1 if (event.key() == Qt.Key_PageDown) else -1) # If not paging off either end, show that page if pgix >= 0 and pgix < self.page_data.page_count() : self._show_page(pgix) if self.image_to_cursor.isChecked(): self.edit_view.show_position(self.page_data.position(pgix)) # Zoom to width and zoom to height are basically the same thing: # 1. Using the QImage of the current page in self.image, # scan its pixels to find the width (height) of the nonwhite area. # 2. Get the ratio of that to our image label's viewport width (height). # 3. Set that ratio as the zoom factor and redraw the image. # 5. Set the scroll position(s) of our scroll area to left-justify the text. # # We get access to the pixel data using QImage.bits() which gives us a # "sip.voidptr" object that we can index to get byte values. def _zoom_to_width(self): # Generic loop to scan inward from the left or right edge of one # column inward until a dark pixel is seen, returning that margin. def inner_loop(row_range, col_start, margin, col_step): pa, pb = 255, 255 # virtual white outside column for row in row_range: for col in range(col_start, margin, col_step): pc = color_table[ bytes_ptr[row+col] ] if (pa + pb + pc) < 24 : # black or dark gray trio margin = col # new, narrower, margin break # no need to look further on this row pa, pb = pb, pc # else shift 3-pixel window return margin - (2*col_step) # allow for 3-px window if self.no_image or self.image.isNull() : return # nothing to do scale_factor = 4 orig_rows = self.image.height() # number of pixels high orig_cols = self.image.width() # number of logical pixels across # Scale the image to 1/4 size (1/16 the pixel count) and then force # it to indexed-8 format, one byte per pixel. work_image = self.image.scaled( QSize(int(orig_cols/scale_factor),int(orig_rows/scale_factor)), Qt.KeepAspectRatio, Qt.FastTransformation) work_image = work_image.convertToFormat(QImage.Format_Indexed8,Qt.ColorOnly) # Get a reduced version of the color table by extracting just the GG # values of each entry, as a dict keyed by the pixel byte value. For # PNG-2, this gives [0,255] but it could have 8, 16, even 256 elements. color_table = { bytes([c]): int((work_image.color(c) >> 8) & 255) for c in range(work_image.colorCount()) } # Establish limits for the inner loop rows = work_image.height() # number of pixels high cols = work_image.width() # number of logical pixels across stride = (cols + 3) & (-4) # scan-line width in bytes bytes_ptr = work_image.bits() # uchar * a_bunch_o_pixels bytes_ptr.setsize(stride * rows) # make the pointer indexable # Scan in from left and from right to find the outermost dark spots. # Pages tend to start with many lines of white pixels so in hopes of # establishing a narrow margin quickly, scan from the middle to the # end, then do the top half. left_margin = inner_loop( range(int(rows/2)*stride, (rows-1)*stride, stride*2), 0, int(cols/2), 1 ) left_margin = inner_loop( range(0, int(rows/2)*stride, stride*2), 0, left_margin, 1 ) # Now do exactly the same but for the right margin. right_margin = inner_loop( range(int(rows/2)*stride, (rows-1)*stride, stride*2), cols-1, int(cols/2), -1 ) right_margin = inner_loop( range(0, int(rows/2)*stride, stride*2), cols-1, right_margin, -1 ) # Adjust the margins by the scale factor to fit the full size image. #left_margin = max(0,left_margin*scale_factor-scale_factor) #right_margin = min(orig_cols,right_margin*scale_factor+scale_factor) left_margin = left_margin*scale_factor right_margin = right_margin*scale_factor text_size = right_margin - left_margin + 2 port_width = self.scroll_area.viewport().width() # Set the new zoom factor, after limiting by min/max values self._set_zoom_real(port_width/text_size) # Set the scrollbar to show the page from its left margin. self.scroll_area.horizontalScrollBar().setValue( int( left_margin * self.zoom_factor) ) # and that completes zoom-to-width def _zoom_to_height(self): def dark_row(row_start, cols): ''' Scan one row of pixels and return True if it contains at least one 3-pixel blob of darkness, or False if not. ''' pa, pb = 255, 255 for c in range(row_start,row_start+cols): pc = color_table[ bytes_ptr[c] ] if (pa + pb + pc) < 24 : # black or dark gray trio return True pa, pb = pb, pc return False # row was all-white-ish if self.no_image or self.image.isNull() : return # nothing to do scale_factor = 4 orig_rows = self.image.height() # number of pixels high orig_cols = self.image.width() # number of logical pixels across # Scale the image to 1/4 size (1/16 the pixel count) and then force # it to indexed-8 format, one byte per pixel. work_image = self.image.scaled( QSize(int(orig_cols/scale_factor),int(orig_rows/scale_factor)), Qt.KeepAspectRatio, Qt.FastTransformation) work_image = work_image.convertToFormat(QImage.Format_Indexed8,Qt.ColorOnly) # Get a reduced version of the color table by extracting just the GG # values of each entry, as a dict keyed by the pixel byte value. For # PNG-2, this gives [0,255] but it could have 8, 16, even 256 elements. color_table = { bytes([c]): int((work_image.color(c) >> 8) & 255) for c in range(work_image.colorCount()) } rows = work_image.height() # number of pixels high cols = work_image.width() # number of logical pixels across stride = (cols + 3) & (-4) # scan-line width in bytes bytes_ptr = work_image.bits() # uchar * a_bunch_o_pixels bytes_ptr.setsize(stride * rows) # make the pointer indexable # Scan the image rows from the top down looking for one with darkness for top_row in range(rows): if dark_row(top_row*stride, cols): break if top_row > (rows/2) : # too much white, skip it return for bottom_row in range(rows-1, top_row, -1): if dark_row(bottom_row*stride, cols) : break # bottom_row has to be >= top_row. if they are too close together # set_zoom_real will limit the zoom to 200%. top_row = top_row*scale_factor bottom_row = bottom_row*scale_factor text_height = bottom_row - top_row + 1 port_height = self.scroll_area.viewport().height() self._set_zoom_real(port_height/text_height) self.scroll_area.verticalScrollBar().setValue( int( top_row * self.zoom_factor ) ) # and that completes zoom-to-height # Build the widgetary contents. The widget consists mostly of a vertical # layout with two items: A scrollArea containing a QLabel used to display # an image, and a horizontal layout containing the zoom controls. # TODO: figure out design and location of two cursor-link tool buttons. def _uic(self): # Function to return the actual width of the label text # of a widget. Get the fontMetrics and ask it for the width. def _label_width(widget): fm = widget.fontMetrics() return fm.width(widget.text()) # Create a gray field to use when no image is available self.gray_image = QPixmap(700,900) self.gray_image.fill(QColor("gray")) # Build the QLabel that displays the image pixmap. It gets all # available space and scales its contents to fit that space. self.image_display = QLabel() self.image_display.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) self.image_display.setScaledContents(True) # Create a scroll area within which to display the image. It will # create a horizontal and/or vertical scroll bar when # the image_display size exceeds the size of the scroll area. self.scroll_area = QScrollArea() self.scroll_area.setBackgroundRole(QPalette.Dark) self.scroll_area.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContents) self.scroll_area.setWidget(self.image_display) # Make sure the scroll area does not swallow user keystrokes self.setFocusPolicy(Qt.ClickFocus) # focus into whole widget self.scroll_area.setFocusProxy(self) # you, pass it on. # Create the image-linking toolbuttons. # Cursor-to-image uses left-hands. c2i_on = QPixmap(':/hand-left-closed.png') c2i_off = QPixmap(':/hand-left-open.png') c2i_con = QIcon() c2i_con.addPixmap(c2i_on,QIcon.Normal,QIcon.On) c2i_con.addPixmap(c2i_off,QIcon.Normal,QIcon.Off) self.cursor_to_image = QToolButton() self.cursor_to_image.setCheckable(True) self.cursor_to_image.setContentsMargins(0,0,0,0) self.cursor_to_image.setIconSize(QSize(30,24)) self.cursor_to_image.setMaximumSize(QSize(32,26)) self.cursor_to_image.setIcon(c2i_con) # Image-to-cursor uses right-hands. i2c_on = QPixmap(':/hand-right-closed.png') i2c_off = QPixmap(':/hand-right-open.png') i2c_con = QIcon() i2c_con.addPixmap(i2c_on,QIcon.Normal,QIcon.On) i2c_con.addPixmap(i2c_off,QIcon.Normal,QIcon.Off) self.image_to_cursor = QToolButton() self.image_to_cursor.setCheckable(True) self.image_to_cursor.setContentsMargins(0,0,0,0) self.image_to_cursor.setIconSize(QSize(30,24)) self.image_to_cursor.setMaximumSize(QSize(32,26)) self.image_to_cursor.setIcon(i2c_con) # Create a spinbox to set the zoom from 15 to 200 and connect its # signal to our slot. self.zoom_pct = QSpinBox() self.zoom_pct.setRange( int(100*ZOOM_FACTOR_MIN),int(100*ZOOM_FACTOR_MAX)) self.zoom_pct.setToolTip( _TR('Imageview zoom control tooltip', 'Set the magnification of the page image') ) # Connect the valueChanged(int) signal as opposed to the # valueChanged(str) signal. self.zoom_pct.valueChanged['int'].connect(self._new_zoom_pct) # Create a label for the zoom spinbox. (the label is not saved as a # class member, its layout will keep it in focus) Not translating # the word "Zoom". pct_label = QLabel( '&Zoom {0}-{1}%'.format( str(self.zoom_pct.minimum() ), str(self.zoom_pct.maximum() ) ) ) pct_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) pct_label.setBuddy(self.zoom_pct) # Create the to-width and to-height zoom buttons. Make # sure their widths are equal after translation. self.zoom_to_width = QPushButton( _TR('Imageview zoom control button name','to Width') ) self.zoom_to_width.setToolTip( _TR('Imageview zoom control tooltip', 'Adjust the image to fill the window side to side.') ) self.zoom_to_width.clicked.connect(self._zoom_to_width) self.zoom_to_height = QPushButton( _TR('Imageview zoom control button name','to Height') ) self.zoom_to_height.setToolTip( _TR('Imageview zoom control tooltip', 'Adjust the image to fill the window top to bottom.') ) self.zoom_to_height.clicked.connect(self._zoom_to_height) w = 20 + max(_label_width(self.zoom_to_height),_label_width(self.zoom_to_width)) self.zoom_to_height.setMinimumWidth(w) self.zoom_to_width.setMinimumWidth(w) # Create an HBox for the top of the panel which contains only # the cursor-to-image link button. tophbox = QHBoxLayout() tophbox.setContentsMargins(0,0,0,0) tophbox.addWidget(self.cursor_to_image,0) tophbox.addStretch() # left-align the button # Create an HBox layout to contain the above controls, using # spacers left and right to center them and a spacers between # to control the spacing. zhbox = QHBoxLayout() zhbox.setContentsMargins(0,0,0,0) zhbox.addWidget(self.image_to_cursor,0) zhbox.addStretch(2) # left and right spacers have stretch 2 zhbox.addWidget(pct_label,0) zhbox.addWidget(self.zoom_pct,0) zhbox.addStretch(1) # spacers between widgets are stretch 1 zhbox.addWidget(self.zoom_to_height,0) zhbox.addSpacing(10) # juuuust a little space between buttons zhbox.addWidget(self.zoom_to_width,0) zhbox.addStretch(2) # right side spacer # With all the pieces in hand, create our layout with a stack of # image over row of controls. vbox = QVBoxLayout() vbox.setContentsMargins(0,0,0,0) vbox.addLayout(tophbox,0) # The image gets a high stretch and default alignment. vbox.addWidget(self.scroll_area,2) vbox.addLayout(zhbox,0) self.setLayout(vbox) # And that completes the UI setup.
def related_output(FBTS, suffix, encoding=None): qd = QDir(FBTS.folderpath()) target = FBTS.filename() + '.' + suffix a_file = QFile(qd.absoluteFilePath(target)) return _qfile_to_stream(a_file, QIODevice.WriteOnly, encoding)
def related_output(FBTS, suffix, encoding=None): qd = QDir( FBTS.folderpath() ) target = FBTS.filename() + '.' + suffix a_file = QFile( qd.absoluteFilePath(target) ) return _qfile_to_stream(a_file, QIODevice.WriteOnly, encoding)
def set_path(self, book_folder_path): book_dir = QDir(book_folder_path) if book_dir.exists('pngs'): self.png_dir = QDir(book_dir.absoluteFilePath('pngs')) self._enable() self.cursor_move()
class ImageDisplay(QWidget): def __init__(self, my_book, parent=None): super().__init__(parent) self.my_book = my_book # register metadata readers and writers md = my_book.get_meta_manager() md.register(C.MD_IZ, self._zoom_read, self._zoom_write) md.register(C.MD_IX, self._link_read, self._link_write) # Create our widgets including cursor_to_image and # image_to_cursor pushbuttons. self._uic() # set defaults in case no metadata self.cursor_to_image.toggled.connect(self.toggle_cursor_to_image) self.cursor_to_image.setChecked(True) self.image_to_cursor.toggled.connect(self.toggle_image_to_cursor) self.image_to_cursor.setChecked(False) self.zoom_factor = 0.25 self.png_path = None # disable all widgetry until we get some metadata self._disable() # end of __init__() # Disable our widgets because we have no image to show. def _disable(self): self.no_image = True self.last_index = None # compares unequal to any self.pix_map = QPixmap() self.image = QImage() self.cursor_to_image.setEnabled(False) self.image_to_cursor.setEnabled(False) self.zoom_pct.setEnabled(False) self.zoom_to_width.setEnabled(False) self.zoom_to_height.setEnabled(False) self.image_display.setPixmap(self.gray_image) self.image_display.setToolTip( _TR('Image view tooltip', 'Display of one scanned page (no images available)')) # Enable our widgets, we have images to show. At this time the Book # has definitely created an edit view and a page model. def _enable(self): self.edit_view = self.my_book.get_edit_view() self.editor = self.edit_view.Editor # access to actual QTextEdit self.page_data = self.my_book.get_page_model() self.cursor_to_image.setEnabled(True) self.image_to_cursor.setEnabled(True) self.zoom_to_width.setEnabled(True) self.zoom_to_height.setEnabled(True) self.image_display.setToolTip('') #self.image_display.setToolTip( #_TR('Image view tooltip', #'Display of one scanned page from the book') #) self.no_image = False self.zoom_pct.setEnabled(True) # the following triggers entry to _new_zoom_pct() below self.zoom_pct.setValue(int(100 * self.zoom_factor)) # Metadata: read or write the value of the current image zoom factor as a # decimal between 0.15 and 2.0. On input defend against user mistakes. def _zoom_read(self, section, value, version): try: z = float(value) # throws exception on non-numeric value if math.isnan(z) or (z < 0.15) or (z > 2.0): raise ValueError self.zoom_factor = z except: imageview_logger.error( 'Invalid IMAGEZOOM "{}" ignored'.format(value)) def _zoom_write(self, section): return self.zoom_factor # Metadata: read or write the values of the image_to_cursor and # cursor_to_image switches as a list [cursor_to_image, image_to_cursor]. # On input defend against user meddling. Actually bool(x) is pretty lax, # any scalar not zero and any iterable not empty is True. Zero and # iterables of length 0 are False. def _link_read(self, section, value, version): try: (c2i, i2c) = value # exception if not iterable of 2 items self.cursor_to_image.setChecked(bool(c2i)) self.image_to_cursor.setChecked(bool(i2c)) except: imageview_logger.error( 'Invalid IMAGELINKING "{}" ignored'.format(value)) def _link_write(self, section): return [ self.cursor_to_image.isChecked(), self.image_to_cursor.isChecked() ] # The Book calls here after it has loaded a book with defined page data, # passing the path to the folder containing the book. If we can find a # folder named 'pngs' in it we record that path and enable our widgets, # and fake a cursorMoved signal to display the current edit page. def set_path(self, book_folder_path): book_dir = QDir(book_folder_path) if book_dir.exists('pngs'): self.png_dir = QDir(book_dir.absoluteFilePath('pngs')) self._enable() self.cursor_move() # Come here to display or re-display an image. The last-displayed # page image index (if any) is in self.last_index. The desired page # index is passed as the argument, which may be: # * the same as last_index, for example on a change of zoom%. Just # redisplay the current page. # * negative or None if the cursor is "above" the first available page or on # a Page-Up keystroke. Display the gray image. # * greater than page_data.page_count() on a Page-Down keystroke, # display the last available page. # If different from last_index, try to load the .png file for that # page. If that fails, use the gray image. Otherwise display that # page and save it as last_index. def _show_page(self, page_index): if page_index != self.last_index: self.last_index = page_index # change of page, see if we have a filename for it self.pix_map = self.gray_image # assume failure... im_name = self.page_data.filename(page_index) if im_name: # pagedata has a filename; of course there is no guarantee # such a file exists now or ever did. f_name = im_name + '.png' if not self.png_dir.exists(f_name): f_name = im_name + '.jpg' if self.png_dir.exists(f_name): self.image = QImage(self.png_dir.absoluteFilePath(f_name)) if not self.image.isNull(): # we loaded it ok, make a full-scale pixmap for display self.pix_map = QPixmap.fromImage( self.image, Qt.ColorOnly) # Whether new page or not, rescale to current zoom. The .resize method # takes a QSize; pix_map.size() returns one, and it supports * by a real. self.image_display.setPixmap(self.pix_map) self.image_display.resize(self.zoom_factor * self.pix_map.size()) # Slot to receive the cursorMoved signal from the editview widget. If we # are in no_image state, do nothing. If the cursor_to_image switch is # not checked, do nothing. Else get the character position of # the high-end of the current edit selection, and use that to get the # current page index from pagedata, and pass that to _show_page. def cursor_move(self): if self.no_image: return if self.cursor_to_image.isChecked(): pos = self.editor.textCursor().selectionEnd() self._show_page(self.page_data.page_index(pos)) # Slots to receive the signals from our zoom percent and zoom-to buttons. # The controls are disabled while we are in no_image state, so if a signal # arrives, we are not in that state. # # These are strictly internal hence _names. # Any change in the value of the zoom % spin-box including setValue(). def _new_zoom_pct(self, new_value): self.zoom_factor = self.zoom_pct.value() / 100 self._show_page(self.last_index) # Set a new zoom factor (a real) and update the zoom pct spinbox. # Setting zoom_pct triggers a signal to _new_zoom_pct above, and # thence to _show_page which repaints the page at the new scale value. def _set_zoom_real(self, new_value): zoom = max(new_value, ZOOM_FACTOR_MIN) zoom = min(zoom, ZOOM_FACTOR_MAX) self.zoom_factor = zoom self.zoom_pct.setValue(int(100 * zoom)) # Re-implement keyPressEvent in order to provide zoom and page up/down. # ctrl-plus increases the image size by 1.25 # ctrl-minus decreases the image size by 0.8 # page-up displays the next-higher page # page-down displays the next-lower page def keyPressEvent(self, event): # assume we will not handle this key and clear its accepted flag event.ignore() if self.no_image or (self.last_index is None): return # ignore keys until we are showing some image # We have images to show, check the key value. modkey = int( int(event.key() | (int(event.modifiers()) & C.KEYPAD_MOD_CLEAR))) if modkey in C.KEYS_ZOOM: event.accept() fac = (0.8) if (modkey == C.CTL_MINUS) else (1.25) self._set_zoom_real(fac * self.zoom_factor) elif (event.key() == Qt.Key_PageUp) or (event.key() == Qt.Key_PageDown): event.accept() pgix = self.last_index + (1 if (event.key() == Qt.Key_PageDown) else -1) # If not paging off either end, show that page if pgix >= 0 and pgix < self.page_data.page_count(): self._show_page(pgix) if self.image_to_cursor.isChecked(): self.edit_view.show_position(self.page_data.position(pgix)) # Zoom to width and zoom to height are basically the same thing: # 1. Using the QImage of the current page in self.image, # scan its pixels to find the width (height) of the nonwhite area. # 2. Get the ratio of that to our image label's viewport width (height). # 3. Set that ratio as the zoom factor and redraw the image. # 5. Set the scroll position(s) of our scroll area to left-justify the text. # # We get access to the pixel data using QImage.bits() which gives us a # "sip.voidptr" object that we can index to get byte values. def _zoom_to_width(self): # Generic loop to scan inward from the left or right edge of one # column inward until a dark pixel is seen, returning that margin. def inner_loop(row_range, col_start, margin, col_step): pa, pb = 255, 255 # virtual white outside column for row in row_range: for col in range(col_start, margin, col_step): pc = color_table[bytes_ptr[row + col]] if (pa + pb + pc) < 24: # black or dark gray trio margin = col # new, narrower, margin break # no need to look further on this row pa, pb = pb, pc # else shift 3-pixel window return margin - (2 * col_step) # allow for 3-px window if self.no_image or self.image.isNull(): return # nothing to do scale_factor = 4 orig_rows = self.image.height() # number of pixels high orig_cols = self.image.width() # number of logical pixels across # Scale the image to 1/4 size (1/16 the pixel count) and then force # it to indexed-8 format, one byte per pixel. work_image = self.image.scaled( QSize(int(orig_cols / scale_factor), int(orig_rows / scale_factor)), Qt.KeepAspectRatio, Qt.FastTransformation) work_image = work_image.convertToFormat(QImage.Format_Indexed8, Qt.ColorOnly) # Get a reduced version of the color table by extracting just the GG # values of each entry, as a dict keyed by the pixel byte value. For # PNG-2, this gives [0,255] but it could have 8, 16, even 256 elements. color_table = { bytes([c]): int((work_image.color(c) >> 8) & 255) for c in range(work_image.colorCount()) } # Establish limits for the inner loop rows = work_image.height() # number of pixels high cols = work_image.width() # number of logical pixels across stride = (cols + 3) & (-4) # scan-line width in bytes bytes_ptr = work_image.bits() # uchar * a_bunch_o_pixels bytes_ptr.setsize(stride * rows) # make the pointer indexable # Scan in from left and from right to find the outermost dark spots. # Pages tend to start with many lines of white pixels so in hopes of # establishing a narrow margin quickly, scan from the middle to the # end, then do the top half. left_margin = inner_loop( range(int(rows / 2) * stride, (rows - 1) * stride, stride * 2), 0, int(cols / 2), 1) left_margin = inner_loop(range(0, int(rows / 2) * stride, stride * 2), 0, left_margin, 1) # Now do exactly the same but for the right margin. right_margin = inner_loop( range(int(rows / 2) * stride, (rows - 1) * stride, stride * 2), cols - 1, int(cols / 2), -1) right_margin = inner_loop(range(0, int(rows / 2) * stride, stride * 2), cols - 1, right_margin, -1) # Adjust the margins by the scale factor to fit the full size image. #left_margin = max(0,left_margin*scale_factor-scale_factor) #right_margin = min(orig_cols,right_margin*scale_factor+scale_factor) left_margin = left_margin * scale_factor right_margin = right_margin * scale_factor text_size = right_margin - left_margin + 2 port_width = self.scroll_area.viewport().width() # Set the new zoom factor, after limiting by min/max values self._set_zoom_real(port_width / text_size) # Set the scrollbar to show the page from its left margin. self.scroll_area.horizontalScrollBar().setValue( int(left_margin * self.zoom_factor)) # and that completes zoom-to-width def _zoom_to_height(self): def dark_row(row_start, cols): ''' Scan one row of pixels and return True if it contains at least one 3-pixel blob of darkness, or False if not. ''' pa, pb = 255, 255 for c in range(row_start, row_start + cols): pc = color_table[bytes_ptr[c]] if (pa + pb + pc) < 24: # black or dark gray trio return True pa, pb = pb, pc return False # row was all-white-ish if self.no_image or self.image.isNull(): return # nothing to do scale_factor = 4 orig_rows = self.image.height() # number of pixels high orig_cols = self.image.width() # number of logical pixels across # Scale the image to 1/4 size (1/16 the pixel count) and then force # it to indexed-8 format, one byte per pixel. work_image = self.image.scaled( QSize(int(orig_cols / scale_factor), int(orig_rows / scale_factor)), Qt.KeepAspectRatio, Qt.FastTransformation) work_image = work_image.convertToFormat(QImage.Format_Indexed8, Qt.ColorOnly) # Get a reduced version of the color table by extracting just the GG # values of each entry, as a dict keyed by the pixel byte value. For # PNG-2, this gives [0,255] but it could have 8, 16, even 256 elements. color_table = { bytes([c]): int((work_image.color(c) >> 8) & 255) for c in range(work_image.colorCount()) } rows = work_image.height() # number of pixels high cols = work_image.width() # number of logical pixels across stride = (cols + 3) & (-4) # scan-line width in bytes bytes_ptr = work_image.bits() # uchar * a_bunch_o_pixels bytes_ptr.setsize(stride * rows) # make the pointer indexable # Scan the image rows from the top down looking for one with darkness for top_row in range(rows): if dark_row(top_row * stride, cols): break if top_row > (rows / 2): # too much white, skip it return for bottom_row in range(rows - 1, top_row, -1): if dark_row(bottom_row * stride, cols): break # bottom_row has to be >= top_row. if they are too close together # set_zoom_real will limit the zoom to 200%. top_row = top_row * scale_factor bottom_row = bottom_row * scale_factor text_height = bottom_row - top_row + 1 port_height = self.scroll_area.viewport().height() self._set_zoom_real(port_height / text_height) self.scroll_area.verticalScrollBar().setValue( int(top_row * self.zoom_factor)) # and that completes zoom-to-height # Two tiny slots to receive the "toggled(bool)" signal of the grippy # hands icons. All we do is swap out the tooltip text to reflect # what they are set to do. Note that during __init__ there is a # setChecked() call on both, which causing a call to these. def toggle_cursor_to_image(self, bool): if bool: self.cursor_to_image.setToolTip( _TR('imageview edit-to-image button tooltip', 'Images change when the edit cursor moves.')) else: self.cursor_to_image.setToolTip( _TR('imageview edit-to-image button tooltip', 'Images do NOT change when the edit cursor moves.')) def toggle_image_to_cursor(self, bool): if bool: self.image_to_cursor.setToolTip( _TR( 'imageview image-to-cursor button tooltip', 'The edit cursor moves when you use Page Up/Down to change images.' )) else: self.image_to_cursor.setToolTip( _TR( 'imageview image-to-cursor button tooltip', 'The edit cursor does NOT move when you use Page Up/Down to change images.' )) # Build the widgetary contents. The widget consists mostly of a vertical # layout with two items: A scrollArea containing a QLabel used to display # an image, and a horizontal layout containing the zoom controls. # TODO: figure out design and location of two cursor-link tool buttons. def _uic(self): # Function to return the actual width of the label text # of a widget. Get the fontMetrics and ask it for the width. def _label_width(widget): fm = widget.fontMetrics() return fm.width(widget.text()) # Create a gray field to use when no image is available self.gray_image = QPixmap(700, 900) self.gray_image.fill(QColor("gray")) # Build the QLabel that displays the image pixmap. It gets all # available space and scales its contents to fit that space. self.image_display = QLabel() self.image_display.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) self.image_display.setScaledContents(True) # Create a scroll area within which to display the image. It will # create a horizontal and/or vertical scroll bar when # the image_display size exceeds the size of the scroll area. self.scroll_area = QScrollArea() self.scroll_area.setBackgroundRole(QPalette.Dark) self.scroll_area.setSizeAdjustPolicy( QAbstractScrollArea.AdjustToContents) self.scroll_area.setWidget(self.image_display) # Make sure the scroll area does not swallow user keystrokes self.setFocusPolicy(Qt.ClickFocus) # focus into whole widget self.scroll_area.setFocusProxy(self) # you, pass it on. # Create the image-linking toolbuttons. # Cursor-to-image uses left-hands. c2i_on = QPixmap(':/hand-left-closed.png') c2i_off = QPixmap(':/hand-left-open.png') c2i_con = QIcon() c2i_con.addPixmap(c2i_on, QIcon.Normal, QIcon.On) c2i_con.addPixmap(c2i_off, QIcon.Normal, QIcon.Off) self.cursor_to_image = QToolButton() self.cursor_to_image.setCheckable(True) self.cursor_to_image.setContentsMargins(0, 0, 0, 0) self.cursor_to_image.setIconSize(QSize(30, 24)) self.cursor_to_image.setMaximumSize(QSize(32, 26)) self.cursor_to_image.setIcon(c2i_con) # Image-to-cursor uses right-hands. i2c_on = QPixmap(':/hand-right-closed.png') i2c_off = QPixmap(':/hand-right-open.png') i2c_con = QIcon() i2c_con.addPixmap(i2c_on, QIcon.Normal, QIcon.On) i2c_con.addPixmap(i2c_off, QIcon.Normal, QIcon.Off) self.image_to_cursor = QToolButton() self.image_to_cursor.setCheckable(True) self.image_to_cursor.setContentsMargins(0, 0, 0, 0) self.image_to_cursor.setIconSize(QSize(30, 24)) self.image_to_cursor.setMaximumSize(QSize(32, 26)) self.image_to_cursor.setIcon(i2c_con) # Create a spinbox to set the zoom from 15 to 200 and connect its # signal to our slot. self.zoom_pct = QSpinBox() self.zoom_pct.setRange(int(100 * ZOOM_FACTOR_MIN), int(100 * ZOOM_FACTOR_MAX)) self.zoom_pct.setToolTip( _TR('Imageview zoom control tooltip', 'Set the magnification of the page image')) # Connect the valueChanged(int) signal as opposed to the # valueChanged(str) signal. self.zoom_pct.valueChanged['int'].connect(self._new_zoom_pct) # Create a label for the zoom spinbox. (the label is not saved as a # class member, its layout will keep it in focus) Not translating # the word "Zoom". pct_label = QLabel('&Zoom {0}-{1}%'.format( str(self.zoom_pct.minimum()), str(self.zoom_pct.maximum()))) pct_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) pct_label.setBuddy(self.zoom_pct) # Create the to-width and to-height zoom buttons. Make # sure their widths are equal after translation. self.zoom_to_width = QPushButton( _TR('Imageview zoom control button name', 'to Width')) self.zoom_to_width.setToolTip( _TR('Imageview zoom control tooltip', 'Adjust the image to fill the window side to side.')) self.zoom_to_width.clicked.connect(self._zoom_to_width) self.zoom_to_height = QPushButton( _TR('Imageview zoom control button name', 'to Height')) self.zoom_to_height.setToolTip( _TR('Imageview zoom control tooltip', 'Adjust the image to fill the window top to bottom.')) self.zoom_to_height.clicked.connect(self._zoom_to_height) w = 20 + max(_label_width(self.zoom_to_height), _label_width(self.zoom_to_width)) self.zoom_to_height.setMinimumWidth(w) self.zoom_to_width.setMinimumWidth(w) # Create an HBox for the top of the panel which contains only # the cursor-to-image link button. tophbox = QHBoxLayout() tophbox.setContentsMargins(0, 0, 0, 0) tophbox.addWidget(self.cursor_to_image, 0) tophbox.addStretch() # left-align the button # Create an HBox layout to contain the above controls, using # spacers left and right to center them and a spacers between # to control the spacing. zhbox = QHBoxLayout() zhbox.setContentsMargins(0, 0, 0, 0) zhbox.addWidget(self.image_to_cursor, 0) zhbox.addStretch(2) # left and right spacers have stretch 2 zhbox.addWidget(pct_label, 0) zhbox.addWidget(self.zoom_pct, 0) zhbox.addStretch(1) # spacers between widgets are stretch 1 zhbox.addWidget(self.zoom_to_height, 0) zhbox.addSpacing(10) # juuuust a little space between buttons zhbox.addWidget(self.zoom_to_width, 0) zhbox.addStretch(2) # right side spacer # With all the pieces in hand, create our layout with a stack of # image over row of controls. vbox = QVBoxLayout() vbox.setContentsMargins(0, 0, 0, 0) vbox.addLayout(tophbox, 0) # The image gets a high stretch and default alignment. vbox.addWidget(self.scroll_area, 2) vbox.addLayout(zhbox, 0) self.setLayout(vbox)