Пример #1
0
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
Пример #2
0
    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()
Пример #3
0
 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()),
             )
Пример #4
0
 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)
Пример #5
0
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
Пример #6
0
    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)
Пример #7
0
 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()
Пример #8
0
    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)
Пример #9
0
    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
Пример #10
0
    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
Пример #11
0
	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()
Пример #12
0
    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)
Пример #14
0
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
Пример #15
0
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
Пример #16
0
    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
Пример #17
0
    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")
Пример #18
0
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()
Пример #19
0
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)
Пример #20
0
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)))
Пример #21
0
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())))
Пример #22
0
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())))
Пример #23
0
 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()
Пример #24
0
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.
Пример #25
0
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)
Пример #26
0
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)
Пример #27
0
 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()
Пример #28
0
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)