• 15 апреля стартует «Курс «SQL-injection Master» ©» от команды The Codeby

    За 3 месяца вы пройдете путь от начальных навыков работы с SQL-запросами к базам данных до продвинутых техник. Научитесь находить уязвимости связанные с базами данных, и внедрять произвольный SQL-код в уязвимые приложения.

    На последнюю неделю приходится экзамен, где нужно будет показать свои навыки, взломав ряд уязвимых учебных сайтов, и добыть флаги. Успешно сдавшие экзамен получат сертификат.

    Запись на курс до 25 апреля. Получить промодоступ ...

Статья Пишем приложение с GUI для конвертации HTML в PDF с помощью Python. Часть 02

В прошлой части мы создали интерфейс приложения, с помощью которого можно конвертировать HTML-файлы в PDF, а также объединять PDF-файлы и сжимать размер. В этой части мы продолжим создавать приложение и будем писать его логику. Файлы из прошлой статьи во вложении к ней. Так что, если вы не читали первую часть, можно просто скачать файлы и продолжить в них.

02.jpg



В конце статьи небольшое видео демонстрирующее работу приложения

Инициализация приложения. Функция проверки наличия пакетов

Создадим функцию update(). На входе она не принимает никаких параметров. Для начала, с помощью shutil.which запросим путь к пакету wkhtmltopdf. Если путь будет найден, все в порядке. Если же нет, сообщим пользователю, что на компьютере отсутствует необходимый для работы приложения пакет и запросим пароль sudo для его установки.

Python:
    if not shutil.which('wkhtmltopdf'):
        sudo_password = QtWidgets.QInputDialog.getText(None, "Пароль для установки", "Внимание!\nНе установлен"
                                                                                     " wkhtmltopdf.\n"
                                                                                     "Для его установки введите "
                                                                                     "пароль sudo:",
                                                       QlineEdit.Normal)[0]

Составим команду, которая необходима для установки пакета и разобьем ее на части по пробелу. Это необходимо для того, чтобы подать корректно данные в функцию Popen. Затем, с помощью Popen запускаем составную команду для установки пакета. В следующей строке, с помощью communicate, передаем полученный пароль терминал.

Python:
        command1 = 'apt install wkhtmltopdf'.split()
        p = subprocess.Popen(['sudo', '-S'] + command1, stdin=subprocess.PIPE, stderr=subprocess.PIPE,
                             universal_newlines=True)
        p.communicate(sudo_password + '\n')

Начнется установка пакета. После чего запуститься приложение. Аналогично проверяем наличие на компьютере установленного GhostScript. В случае его отсутствия устанавливаем.

А запускать данную функцию мы будем из блока if __name__, сразу же после создания экземпляра класса QApplication, виджетов:

Python:
if __name__ == '__main__':
    """
    Создается объект с Qt виджетами, запускается функция проверки пакетов,
    запускается Qt приложение.
    """
    app = QtWidgets.QApplication(sys.argv)
    update()
    myapp = MyWin()
    myapp.show()
    sys.exit(app.exec_())


Инициализация класса приложения

При инициализации класса нужно определить все необходимые переменные, создать окно приложения, определить функции для кнопок и многое другое. Давайте по порядку. Итак, вот код инициализации:

Python:
class MyWin(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        QtWidgets.QWidget.__init__(self, parent)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.path_file = None
        self.ui.mergeBtn.setEnabled(False)
        self.ui.convertBtn.setEnabled(False)
        self.ui.compressBtn.setEnabled(False)
        self.ui.ratioBox.setCurrentText('3')
        self.ui.fileView.horizontalHeader().resizeSection(0, 895)
        self.ui.folderBtn.clicked.connect(self.folder_open)
        self.ui.mergeBtn.clicked.connect(self.merge_pdf)
        self.ui.convertBtn.clicked.connect(self.convert_html)
        self.ui.delBtn.clicked.connect(self.del_line)
        self.ui.upBtn.clicked.connect(self.move_up)
        self.ui.dwnBtn.clicked.connect(self.move_down)
        self.ui.compressBtn.clicked.connect(self.compress_pdf)
        self.ui.methodBox.currentTextChanged.connect(self.change_method)
        self.ui.exitBtn.clicked.connect(self.exit_application)

Давайте чуть подробнее затронем некоторые моменты.
В переменной self.path_file = None я определяю глобальную переменную, которая будет содержать путь к рабочей директории получаемой с помощью кнопки открыть.
С помощью self.ui.mergeBtn.setEnabled(False) и нескольких последующих строк устанавливаем свойство Enable кнопок конвертации и сжатия в False, для того, чтобы они были неактивны при запуске приложения.
Устанавливаем с помощью self.ui.ratioBox.setCurrentText('3') текст равный трем, в данном комбо-боксе выбирается степень сжатия с помощью GhostScript. Именно это значение будет использоваться по умолчанию.
Растягиваем заголовок self.ui.fileView.horizontalHeader().resizeSection(0, 895) почти на всю площадь таблицы. В данном случае я сделал фиксированный заголовок. Однако, возможно было бы определить, чтобы данный заголовок устанавливал размеры в зависимости от длины содержимого ячеек.
Далее идет код, в котором определяется функция для кнопки при нажатии на нее, а также код при выборе в комбо-боксе метода сжатия pdf-файла.

Двигаемся дальше.


Закрытие приложения

На самом деле, эту функцию можно было бы вынести за пределы класса. Но я оставил ее здесь, так как за пределами класса метод закрытия приложения работать не будет. Данная функция срабатывает при нажатии на кнопку закрытия приложения. Назвал я функцию exit_application(self). Вот ее код:

Python:
    def exit_application(self):
        """
        Завершение работы приложения.
        """
        QtWidgets.QApplication.quit()


Перемещение строки в таблице вверх или вниз

Данный код я нашел в интернете и адаптировал под свои нужны. Так что, можно сказать, что я не писал его с нуля. Тем не менее, с помощью двух функций, которые будут представлены ниже, перемещается выбранная в таблице запись вверх или вниз. Для чего это нужно? Это нужно для того, чтобы определить порядок в котором будут объединятся файлы, для примера. Так как они могут быть отсортированы не так, как вам нужно. Поэтому, иногда требуется переместить строки.

Создадим функцию move_down(self). Определим индекс текущей строки и столбца. Теперь делаем проверку, если индекс текущей строки меньше, чем количество строк — 1, вставляем в таблицу строку ниже относительной позиции. В цикле перемещаем содержимое сток, после чего удаляем строку, так как она у нас уже на новой позиции.

Python:
        row = self.ui.fileView.currentRow()
        column = self.ui.fileView.currentColumn()
        if row < self.ui.fileView.rowCount() - 1:
            self.ui.fileView.insertRow(row + 2)
            for i in range(self.ui.fileView.columnCount()):
                self.ui.fileView.setItem(row + 2, i, self.ui.fileView.takeItem(row, i))
                self.ui.fileView.setCurrentCell(row + 2, column)
            self.ui.fileView.removeRow(row)

Python:
    def move_down(self):
        """
        Перемещение строки в таблице вниз.
        Определяем индекс текущей строки и столбца. Сравниваем текущий
        индекс строки. Если он меньше, чем текущий индекс - 1, создаем
        новую строку. Запускаем цикл в диапазоне количества столбцов, перемещаем
        содержимое вниз. После цикла удаляем строку, которую нужно было переместить.
        """
        row = self.ui.fileView.currentRow()
        column = self.ui.fileView.currentColumn()
        if row < self.ui.fileView.rowCount() - 1:
            self.ui.fileView.insertRow(row + 2)
            for i in range(self.ui.fileView.columnCount()):
                self.ui.fileView.setItem(row + 2, i, self.ui.fileView.takeItem(row, i))
                self.ui.fileView.setCurrentCell(row + 2, column)
            self.ui.fileView.removeRow(row)

Теперь создадим функцию move_up(self). Здесь все будет происходить то же самое, только проверка текущей позиции будет проходить относительно 0, так как 0, это первый индекс строки в таблице. А далее создаем строку, но только выше текущей позиции. И в цикле перемещаем содержимое строк вверх.

Python:
    def move_up(self):
        """
        Перемещение строки в таблице вверх. Повторяем все те же операции, но сравниваем
        индекс текущей строки с нулем. Пока он больше, можем перемещать.
        """
        row = self.ui.fileView.currentRow()
        column = self.ui.fileView.currentColumn()
        if row > 0:
            self.ui.fileView.insertRow(row - 1)
            for i in range(self.ui.fileView.columnCount()):
                self.ui.fileView.setItem(row - 1, i, self.ui.fileView.takeItem(row + 1, i))
                self.ui.fileView.setCurrentCell(row - 1, column)
            self.ui.fileView.removeRow(row + 1)


Удаление строки

Если вам понадобиться удалить строки из таблицы, создадим для этого функцию del_line(self). Здесь мы определяем индекс в текущей позиции и удаляем ее из таблицы по индексу.

Python:
    def del_line(self):
        """
        Удаление строки из таблицы.
        Определяем индекс строки, удаляем строку из таблицы
        по индексу.
        """
        row = self.ui.fileView.currentRow()
        self.ui.fileView.removeRow(row)


Конвертация HTML в PDF

Создадим функцию convert_html(self). Для начала, так как у нас есть прогресс-бар, установим его значение в 0. После чего сформируем список ссылок на локальные html-файлы из строк таблицы. Так как в таблице могут быть и файлы pdf, сделаем условие, по которому в список будут добавляться только строки, у которых окончание «.html».

Python:
        self.ui.progressBar.setValue(0)
        files_html = [self.ui.fileView.item(path, 0).text() for path in range(0, self.ui.fileView.rowCount())
                      if self.ui.fileView.item(path, 0).text().endswith(".html")]

Теперь проверяем длину списка. Если она равна нулю, то конвертировать нечего, что мы и сообщаем пользователю выведя надпись в статус-бар, а также показав всплывающее сообщение и выходим из функции.

Python:
        if len(files_html) == 0:
            self.ui.statusbar.showMessage('Нечего конвертировать')
            linux_notify(f'"Нечего конвертировать"')
            return

Затем устанавливаем максимальное значение прогресс-бара равное длине списка. Запускаем по списку цикл. С помощью enumerate забираем индекс элемента и присваиваем текущему положению прогресс-бара + 1, так как первый элемент списка имеет индекс 0. После чего формируем пути к входному html-файлу и путь для файла pdf, который будет получаться на выходе. И наконец, запускаем конвертирование файлов. С помощью функции pdfkit.from_file, в которую передаем первым параметром путь к файлу html, вторым параметром путь, а соответственно и название к файлу pdf. При возникновении ошибки печатаем ее значение в терминал, однако, это не обязательно и продолжаем выполнение цикла. После его окончания выводим в статус-бар сообщение о том, что все файлы конвертированы, и запускаем функцию для считывания содержимого директории и обновления строк таблицы.

Python:
        self.ui.progressBar.setMaximum(len(files_html))
        for num, file in enumerate(files_html):
            self.ui.progressBar.setValue(num + 1)
            html_name = os.path.join(self.path_file, file)
            pdf_name = os.path.join(self.path_file, f'{file[0:-4]}pdf')
            try:
                pdfkit.from_file(html_name, pdf_name)
            except OSError as ex:
                print(ex)
                continue
        self.ui.statusbar.showMessage('Все файлы конвертированы')
        linux_notify(f'"Все файлы конвертированы"')
        self.file_read()

Python:
    def convert_html(self):
        """
        Функция для конвертации локальных страниц html
        в pdf. Устанавливаем значение прогресс-бара на 0.
        Создаем список из значений строк таблицы, то выбираем
        только те записи, которые имеют расширение html.
        Проверяем длину списка. Если 0, выходим из функции,
        в статус-бар пишем сообщение о том, что конвертировать нечего,
        то же сообщение выводим на экран.
        Если длина списка больше 0, устанавливаем максимальное значение
        статус-бара на длину списка, запускаем цикл по списку, с помощью функции
        enumerate получаем индекс элемента, устанавливаем значение статус-бара равным
        индексу + 1.
        Формируем пути к входным html и выходным pdf файлам.
        Запускаем конвертацию из файла, в качестве параметров передаем путь к файлу
        html и путь для выходного файла pdf. Обрабатываем исключение.
        После завершения цикла выводим в статус-бар сообщение о полной конвертации,
        то же сообщение выводим на экран. Запускаем функцию для чтения файлов из
        директории и переформирования содержимого таблицы на экране.
        :return: Выход из функции в случае ошибки.
        """
        self.ui.progressBar.setValue(0)
        files_html = [self.ui.fileView.item(path, 0).text() for path in range(0, self.ui.fileView.rowCount())
                      if self.ui.fileView.item(path, 0).text().endswith(".html")]

        if len(files_html) == 0:
            self.ui.statusbar.showMessage('Нечего конвертировать')
            linux_notify(f'"Нечего конвертировать"')
            return

        self.ui.progressBar.setMaximum(len(files_html))
        for num, file in enumerate(files_html):
            self.ui.progressBar.setValue(num + 1)
            html_name = os.path.join(self.path_file, file)
            pdf_name = os.path.join(self.path_file, f'{file[0:-4]}pdf')
            try:
                pdfkit.from_file(html_name, pdf_name)
            except OSError as ex:
                print(ex)
                continue
        self.ui.statusbar.showMessage('Все файлы конвертированы')
        linux_notify(f'"Все файлы конвертированы"')
        self.file_read()


Объединение PDF-файлов

Изначально, для объединения файлов я хотел использовать библиотеку PyPDF2. Но как показала практика, она работает несколько медленно, хотя и выполняет свою работу. Поэтому я решил использовать библиотеку PyMuPDF, модуль fitz.

Создадим функцию merge_pdf(self). Установим значение прогресс-бара в 0, после чего создадим список из путей к файлам PDF, которые содержаться в таблице. Так как в таблице могут содержаться пути к html-файлам, сделаем условие, в котором укажем, что добавлять в список необходимо только те, у которых расширение «.pdf». Проверим длину списка. Если она равна 0 или 1, выведем сообщение, что объединять нечего и выйдем из функции.

Python:
        self.ui.progressBar.setValue(0)
        files_pdf = [self.ui.fileView.item(path, 0).text() for path in range(0, self.ui.fileView.rowCount())
                     if self.ui.fileView.item(path, 0).text().endswith("pdf")]
        if len(files_pdf) == 0 or len(files_pdf) == 1:
            self.ui.statusbar.showMessage('Нечего объединять')
            linux_notify(f'"Нечего объединять"')
            return

Создадим объект fitz, в который далее будем добавлять все считанные файлы pdf. Сформируем путь к текущей директории, который возьмем из поля текстового ввода формы. Оно у нас неактивно, но содержит путь к файлам в таблице. Выведем диалог сохранения, в котором попросим пользователя ввести имя объединенного файла. Сформируем путь с именем, так как функция диалога возвращает список, в котором на первом месте путь с именем, а на втором расширение со звездочкой. Проверим, ввел ли пользователь имя файла. И если нет, выходим из функции.

Python:
        result = fitz.open()
        path_dir = self.ui.pathEdt.text()
        save_path = QtWidgets.QFileDialog.getSaveFileName(None, "Имя для объединенного файла", path_dir, "*.pdf")
        path_name = f'{save_path[0]}{save_path[1].replace("*", "")}'
        if path_name == '':
            return

Устанавливаем максимальное значение прогресс-бара равное длине сформированного списка. Запускаем цикл по списку, в котором с помощью enumerate будем получать индекс элемента. Так как в таблице содержаться только имена файлов, это вы увидите далее, формируем полный путь к файлу pdf, который состоит из параметра, определенного при инициализации класса и содержащий путь к открытой директории, а также имени файла полученном из списка. Продвигаем прогресс-бар на значение индекса. Здесь мы не будем использовать + 1, для того, чтобы после окончания объединения передать конечное значение индекса. Запускаем fitz в контекстном менеджере with, открываем pdf файл и добавляем в ранее созданный файл result. По окончании цикла сохраняем объединенный файл. Прогресс-бару присваиваем значение полной длины списка файлов. Выводим сообщение об окончании операции. Запускаем функцию для чтения содержимого директории и обновления списка файлов в таблице.

Python:
        self.ui.progressBar.setMaximum(len(files_pdf))
        for num, pdf in enumerate(files_pdf):
            file = os.path.join(self.path_file, pdf)
            self.ui.progressBar.setValue(num)
            with fitz.open(file) as mfile:
                result.insert_pdf(mfile)
        result.save(path_name)
        self.ui.progressBar.setValue(len(files_pdf))
        self.ui.statusbar.showMessage('Объединение завершено')
        linux_notify(f'"Объединение завершено"')
        self.file_read()

Python:
    def merge_pdf(self):
        """
        Функция для слияния файлов pdf из таблицы. Слияние выполняется
        с помощью fitz. Создаем объект fitz, открываем каждый из файлов
        pdf и добавляем в созданные объект. После чего выполняем сохранение.
        Работает в разы быстрее аналогичной функции слияния у PyPDF2.
        :return: Выход из функции.
        """
        self.ui.progressBar.setValue(0)
        files_pdf = [self.ui.fileView.item(path, 0).text() for path in range(0, self.ui.fileView.rowCount())
                     if self.ui.fileView.item(path, 0).text().endswith("pdf")]
        if len(files_pdf) == 0 or len(files_pdf) == 1:
            self.ui.statusbar.showMessage('Нечего объединять')
            linux_notify(f'"Нечего объединять"')
            return
        result = fitz.open()
        path_dir = self.ui.pathEdt.text()
        save_path = QtWidgets.QFileDialog.getSaveFileName(None, "Имя для объединенного файла", path_dir, "*.pdf")
        path_name = f'{save_path[0]}{save_path[1].replace("*", "")}'
        if path_name == '':
            return

        self.ui.progressBar.setMaximum(len(files_pdf))
        for num, pdf in enumerate(files_pdf):
            file = os.path.join(self.path_file, pdf)
            self.ui.progressBar.setValue(num)
            with fitz.open(file) as mfile:
                result.insert_pdf(mfile)
        result.save(path_name)
        self.ui.progressBar.setValue(len(files_pdf))
        self.ui.statusbar.showMessage('Объединение завершено')
        linux_notify(f'"Объединение завершено"')
        self.file_read()


Обработка выбора метода сжатия

На форме есть комбо-бокс, в котором можно выбрать метод сжатия файла pdf. Так как у нас есть второй комбо-бокс, в котором можно указать степень сжатия, изначально он неактивен. Создадим функцию change_method(self), в которой будем обрабатывать выбор метода сжатия с помощью Ghost Script, чтобы в этом случае комбо-бокс становился активным. Проверять будем событие комбо-бокса с методами сжатия currentTextChanged.

Пишем условие, в котором проверяем, если текст в комбо-боксе равен значению «Ghost Script», делаем активным комбо-бокс со степенью сжатия. Если текст изменяется, снова делаем его неактивным.

Python:
    def change_method(self):
        """
        Проверка метода сжатия на форме. В зависимости от этого
        делаем комбо-бокс со степенью сжатия активным или нет.
        :return: ВЫход из функции.
        """
        if self.ui.methodBox.currentText() == 'Ghost Script':
            self.ui.ratioBox.setEnabled(True)
        else:
            self.ui.ratioBox.setEnabled(False)


Проверка выбора метода сжатия

Данная функция срабатывает при нажатии на кнопку сжатия pdf-файла. Создадим функцию compress_pdf(self). Сжимать будем тот файл, что укажет пользователь в таблице с помощью мышки, то есть, текущую строку. Для начала проверяем, является ли выбранный файл pdf. Если нет, сообщаем об этом пользователю и выходим из функции. Также есть вероятность, когда кнопка сжатия будет нажата при не выбранной строке. В этом случае обрабатываем исключение.

Python:
        row = self.ui.fileView.currentRow()
        try:
            if not self.ui.fileView.item(row, 0).text().endswith(".pdf"):
                self.ui.statusbar.showMessage('Выбранный файл не PDF')
                linux_notify(f'"Выбранный файл не PDF"')
                return
        except AttributeError:
            self.ui.statusbar.showMessage('Файл не выбран')
            linux_notify(f'"Файл не выбран"')
            return

Формируем пути к входному и выходному файлам. Путь к выходному файлу будет идентичен, за исключением того, что в конце имени файла добавляется слово compress, чтобы не заменять оригинальный файл.

Python:
        path_file = os.path.join(self.path_file, self.ui.fileView.item(row, 0).text())
        out_file = f'{path_file[0:-4]}_compress.pdf'

Затем проверяем текст в комбо-боксе. В зависимости от текста запускаем на выполнение функцию предназначенную для сжатия этим методом. Для начала в статус-бар выводим сообщение о сжатии, которое дублируем во всплывающем уведомлении. И запускаем одну из функций сжатия, которая возвращает коэффициент сжатия в процентах. После того, как функция закончит свою работу и вернет значение, выводим сообщение в статус-бар и во всплывающее уведомление. После чего запускаем функцию чтения содержимого директории и обновления списка файлов в таблице. Алгоритм запуска одинаков для обеих функций.

Python:
    def compress_pdf(self):
        """
        Обработка значения в комбо-боксе с методом сжатия.
        В зависимости от выбранного метода запускается необходимая
        для выполнения этого метода функци.
        :return: Выход из функции.
        """
        row = self.ui.fileView.currentRow()
        try:
            if not self.ui.fileView.item(row, 0).text().endswith(".pdf"):
                self.ui.statusbar.showMessage('Выбранный файл не PDF')
                linux_notify(f'"Выбранный файл не PDF"')
                return
        except AttributeError:
            self.ui.statusbar.showMessage('Файл не выбран')
            linux_notify(f'"Файл не выбран"')
            return

        path_file = os.path.join(self.path_file, self.ui.fileView.item(row, 0).text())
        out_file = f'{path_file[0:-4]}_compress.pdf'

        if self.ui.methodBox.currentText() == 'Ghost Script':
            self.ui.statusbar.showMessage('Сжатие PDF')
            linux_notify(f'"Сжатие PDF"')
            compress_perc = self.compress_gs(path_file, out_file)
            self.ui.statusbar.showMessage(compress_perc)
            linux_notify(f'"{compress_perc}"')
            self.file_read()
        elif self.ui.methodBox.currentText() == 'PyPDF2':
            self.ui.statusbar.showMessage('Сжатие PDF')
            linux_notify(f'"Сжатие PDF"')
            compress_perc = self.compress_file_pypdf2(path_file, out_file)
            self.ui.statusbar.showMessage(compress_perc)
            linux_notify(f'"{compress_perc}"')
            self.file_read()


Сжатие PDF с помощью Ghost Script

Создадим функцию compress_gs(self, path_file, out_file). На входе она принимает два параметра. Первый — путь к файлу, который нужно сжать, второй — имя и путь для сжатого файла. Объявим переменную power, в которую будет считываться значение из комбо-бокса со степенью сжатия. Затем создадим словарь, в котором определим параметры, необходимые для указания при запуске ghostscript.

Python:
        power = int(self.ui.ratioBox.currentText())
        quality = {
            0: '/default',
            1: '/prepress',
            2: '/printer',
            3: '/ebook',
            4: '/screen'
        }

Проверим, существует ли переданный для сжатия pdf-файл.

Python:
        if not os.path.isfile(path_file):
            return "Ошибка: неверный путь к файлу PDF"

С помощью shutil.which получим путь к ghostscript. Данный путь будет необходим при запуске команды. Затем определяем размер файла до сжатия. С помощью subprocess.call выполняем команду, в которую передаем путь к ghostscript, степень сжатия, имя выходного файла, а также путь к файлу для сжатия.

Python:
        gs = shutil.which('gs')
        initial_size = os.path.getsize(path_file)
        subprocess.call([gs, '-sDEVICE=pdfwrite', '-dCompatibilityLevel=1.4',
                         f'-dPDFSETTINGS={quality[power]}',
                         '-dNOPAUSE', '-dQUIET', '-dBATCH',
                         f'-sOutputFile={out_file}',
                         path_file])

После того, как отработает скрипт получим размер сжатого файла. Вычислим степень сжатия в процентах и возвратим данное значение из функции.

Python:
        final_size = os.path.getsize(out_file)
        ratio = 1 - (final_size / initial_size)
        return f"Compression by {ratio:.0%}"

Python:
    def compress_gs(self, path_file, out_file):
        """
        Сжатие файла pdf с помощью GhostScript.
        Параметр качества берется непосредственно из формы.
        Проверяется, существует ли файл, получаем путь к
        GhostScript. Считываем размер файла до сжатия.
        Запускаем команду для сжатия файла с передачей необходимых параметров.
        Считываем размер файла после сжатия, считаем коэффициент.
        :param path_file: путь к входному файлу.
        :param out_file: путь для выходного файла.
        :return: Возвращает степень сжатия файла в процентах.
        """
        power = int(self.ui.ratioBox.currentText())
        quality = {
            0: '/default',
            1: '/prepress',
            2: '/printer',
            3: '/ebook',
            4: '/screen'
        }

        if not os.path.isfile(path_file):
            return "Ошибка: неверный путь к файлу PDF"

        gs = shutil.which('gs')
        initial_size = os.path.getsize(path_file)
        subprocess.call([gs, '-sDEVICE=pdfwrite', '-dCompatibilityLevel=1.4',
                         f'-dPDFSETTINGS={quality[power]}',
                         '-dNOPAUSE', '-dQUIET', '-dBATCH',
                         f'-sOutputFile={out_file}',
                         path_file])
        final_size = os.path.getsize(out_file)
        ratio = 1 - (final_size / initial_size)
        return f"Compression by {ratio:.0%}"


Сжатие PDF с помощью PyPDF2

Для сжатия с помощью PyPDF2 создадим функцию compress_file_pypdf2(self, path_file, out_file). На вход она получает два параметра. Первый — путь к файлу, который нужно сжать, второй — путь к файлу, который нужно получить на выходе.

У этой функции мы можем использовать прогресс-бар, а значит наблюдать за процессом. Однако, данный метод сжатия, по моим наблюдениям, почти всегда уступает сжатию с помощью ghostscript. Однако, его все же можно использовать как альтернативу. Поэтому, устанавливаем значение прогресс-бар в 0. Определяем размер файла до сжатия. Создаем объект писателя, а затем чтеца, в который передаем путь к файлу для сжатия. Устанавливаем максимальное значение прогресс-бара равное количеству страниц в сжимаемом документе. Запускаем цикл в диапазоне от 0, до количества страниц. Увеличиваем на каждой итерации значение прогресс-бара. Считываем каждую страницу документа, пытаемся сжать с помощью compressContentStreams(), после чего передаем объекту писателя.

После окончания цикла записываем получившийся документ в файл. Считываем размер сжатого файла, вычисляем коэффициент сжатия в процентах и возвращаем из функции.

Python:
    def compress_file_pypdf2(self, path_file, out_file):
        """
        Функция сжатия файла pdf с помощью библиотеки
        PyPDF2.
        Получаем размер файла до сжатия, создаем экземпляр
        класса писателя. Создаем экземпляр класса чтеца, с передачей
        пути к файлу для сжатия.
        Устанавливаем максимальное значение прогресс-бара.
        Запускаем цикл в диапазоне равном кол-ву страниц.
        Считываем каждую страницу, сжимаем, передаем писателю.
        Записываем массив данных страниц в файл.
        :param path_file: путь к входному файлу.
        :param out_file: путь для выходного файла.
        :return: Возвращает степень сжатия в процентах.
        """
        self.ui.progressBar.setValue(0)
        start_size = os.path.getsize(path_file)
        writer = PdfFileWriter()
        reader = PdfFileReader(path_file)
        self.ui.progressBar.setMaximum(reader.numPages)
        for num in range(0, reader.numPages):
            self.ui.progressBar.setValue(num+1)
            page = reader.getPage(num)
            page.compressContentStreams()
            writer.addPage(page)
        with open(out_file, 'wb') as file:
            writer.write(file)

        compress_size = os.path.getsize(out_file)
        compression_ratio = 1 - (compress_size / start_size)
        return f'Compression by: {compression_ratio:.0%}'


Чтение файлов в директории, обновление содержимого таблицы

Создадим функцию file_read(self). Создадим список содержимого директории, с условием, при котором в список будем добавлять пути только к тем файлам, расширение которых является «.pdf» или «.html». Удалим содержимое таблицы.

Python:
        files = [x for x in Path(self.path_file).iterdir() if x.suffix == ".pdf" or x.suffix == ".html"]
        self.ui.fileView.setRowCount(0)

Проверим длину списка и если она равна 0, завершим выполнение функции.

Python:
        if len(files) == 0:
            self.ui.statusbar.showMessage('Нет файлов для обработки')
            linux_notify('"Нет файлов для обработки"')
            return

Запустим цикл по сформированному списку файлов, определим текущую позицию в таблице, вставим в нее строку. Добавим в созданную строку значение элементов списка, а именно названия файлов без путей к ним. Очистим содержимое списка files.

Python:
        for fil in sorted(files):
            row_position = self.ui.fileView.rowCount()
            self.ui.fileView.insertRow(row_position)
            self.ui.fileView.setItem(row_position, 0, QtWidgets.QTableWidgetItem(str(fil.name)))
        files.clear()

Python:
    def file_read(self):
        """
        Считываем содержимое полученной ранее директории с файлами.
        Добавляем в список все файлы pdf и html.
        Запускаем цикл по отсортированному списку, добавляем названия файлов
        таблицу, очищаем список файлов.
        """
        files = [x for x in Path(self.path_file).iterdir() if x.suffix == ".pdf" or x.suffix == ".html"]
        self.ui.fileView.setRowCount(0)

        if len(files) == 0:
            self.ui.statusbar.showMessage('Нет файлов для обработки')
            linux_notify('"Нет файлов для обработки"')
            return

        for fil in sorted(files):
            row_position = self.ui.fileView.rowCount()
            self.ui.fileView.insertRow(row_position)
            self.ui.fileView.setItem(row_position, 0, QtWidgets.QTableWidgetItem(str(fil.name)))
        files.clear()


Обработка нажатия кнопки открыть директорию

Создадим функцию folder_open(self), в которой будем обрабатывать нажатие кнопки открытия директории. Для начала установим значения статус-бара в пусто, максимальное значение прогресс-бара в 100 по умолчанию, текущее значение в 0. Запустим диалог открытия директории с файлами. Передадим полученное значение в строку редактирования текста, которая на форме выполняет значение вывода пути к директории.

Python:
        self.ui.statusbar.showMessage('')
        self.ui.progressBar.setMaximum(100)
        self.ui.progressBar.setValue(0)

        self.path_file = QtWidgets.QFileDialog.getExistingDirectory(None, "Выбрать папку", ".")
        self.ui.pathEdt.setText(self.path_file)

Теперь проверим, не нажал ли пользователь кнопку отмена, так как в этом случае значение переменной будет пустым, а мы не можем работать с пустым значением. Если значение не пусто, запускаем функцию чтения файлов в директории и добавления списка файлов в таблицу. Делаем активными кнопки конвертации, объединения и сжатия, а также делаем активным комбо-бокс с выбором метода сжатия. Если же значение пусто, устанавливаем значение статус-бара, в которое выводим сообщение о том, что директория не выбрана, удаляем все значение из поля ввода на форме, и устанавливаем фокус на кнопке открытия директории, после чего выходим из функции.

Python:
        if self.path_file != '':
            self.file_read()
            self.ui.mergeBtn.setEnabled(True)
            self.ui.convertBtn.setEnabled(True)
            self.ui.compressBtn.setEnabled(True)
            self.ui.methodBox.setEnabled(True)
        else:
            self.ui.statusbar.showMessage('Не выбрана директория с файлами')
            linux_notify(f'"Не выбрана директория с файлами"')
            self.ui.pathEdt.setText('')
            self.ui.folderBtn.setFocus()
            return

Python:
    def folder_open(self):
        """
        Функция для получения пути к директории для открытия.
        Устанавливаем значения статус-бара, прогресс-бара.
        Получаем из диалога путь к директории, добавлем его в
        текстовую строку. Проверяем, не является ли полученное значение
        пустым, так как пользователь может передумать и нажать кнопку отмена.
        Если нет, активируем кнопки конвертации, слияния и сжатия.
        Запускаем функцию чтения содержимого директории.
        :return: Выход из функции.
        """
        self.ui.statusbar.showMessage('')
        self.ui.progressBar.setMaximum(100)
        self.ui.progressBar.setValue(0)

        self.path_file = QtWidgets.QFileDialog.getExistingDirectory(None, "Выбрать папку", ".")
        self.ui.pathEdt.setText(self.path_file)

        if self.path_file != '':
            self.file_read()
            self.ui.mergeBtn.setEnabled(True)
            self.ui.convertBtn.setEnabled(True)
            self.ui.compressBtn.setEnabled(True)
            self.ui.methodBox.setEnabled(True)
        else:
            self.ui.statusbar.showMessage('Не выбрана директория с файлами')
            linux_notify(f'"Не выбрана директория с файлами"')
            self.ui.pathEdt.setText('')
            self.ui.folderBtn.setFocus()
            return


Вывод всплывающих уведомлений

Для того, чтобы отображать всплывающие уведомления создадим функцию linux_notify(text), которая принимает на входе текст сообщения. Данную функцию мы разместим вне класса. Для начала формируем команду для вывода сообщения, затем с помощью system ее выполняем.

Python:
def linux_notify(text):
    """
    Функция выводит сообщение с текстом на экран.
    Получаем текст, формируем команду, выполняем.
    :param text: Текстовая строка для сообщения.
    """
    command = f'''notify-send {text}'''
    system(command)

Вот и все. На этом создание приложения можно закончить. Если кто-то захочет добавить функции, то все просто. Кидаете кнопку на форму, пишете обработчик и все работает. К примеру, можно добавить объединение файлов изображений в pdf. Почему именно в pdf? Чтобы не выбиваться из общей концепции, так как здесь, в основном идет работа либо по созданию, либо по обработке pdf-файлов. Впрочем, все в ваших руках.

Мне, на данный момент, того функционала, что я сделал в приложении достаточно для обработки файлов.

Python:
"""
Небольшое приложение с GUI, которое выполняет конвертацию
локальных страниц html в pdf, объединяет pdf в один файл, а также
пытается уменьшить размер указанного файла pdf.
Требует для установки следующие библиотеки:
pip install pyqt5 pyqt5-tools PyPDF2 pdfkit PyMuPDF,
а также необходимо установить пакет для конвертации html:
sudo apt-get install wkhtmltopdf

Данный код предназначен в первую очередь для ОС linux, так как
для Windows приложений подобного рода хватает. Если возникнет необходимость
использовать данный код в ОС Windows, нужно будет изменить модуль для вывода
всплывающих сообщений, установить GhostScript, а также wkhtmltopdf.
С последним эксперименты не проводил, потому не знаю, как он будет работать
в Windows. Однако, есть примеры, в которых можно указать путь к файлу exe и
все будет работать.

В Linux также есть возможность указать путь к исполняемому файлу, но лучше
установить пакет в систему. Так как, в случае использования не установленного
пакета он работает, но вероятность ошибок при конвертации возрастает.
"""

import os.path
import shutil
import subprocess
import sys
from os import system
from pathlib import Path

import fitz
import pdfkit
from PyPDF2 import PdfFileReader, PdfFileWriter
from PyQt5.QtWidgets import QLineEdit

from mergepdf import *


class MyWin(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        QtWidgets.QWidget.__init__(self, parent)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.path_file = None
        self.ui.mergeBtn.setEnabled(False)
        self.ui.convertBtn.setEnabled(False)
        self.ui.compressBtn.setEnabled(False)
        self.ui.ratioBox.setCurrentText('3')
        self.ui.fileView.horizontalHeader().resizeSection(0, 895)
        self.ui.folderBtn.clicked.connect(self.folder_open)
        self.ui.mergeBtn.clicked.connect(self.merge_pdf)
        self.ui.convertBtn.clicked.connect(self.convert_html)
        self.ui.delBtn.clicked.connect(self.del_line)
        self.ui.upBtn.clicked.connect(self.move_up)
        self.ui.dwnBtn.clicked.connect(self.move_down)
        self.ui.compressBtn.clicked.connect(self.compress_pdf)
        self.ui.methodBox.currentTextChanged.connect(self.change_method)
        self.ui.exitBtn.clicked.connect(self.exit_application)

    def exit_application(self):
        """
        Завершение работы приложения.
        """
        QtWidgets.QApplication.quit()

    def move_down(self):
        """
        Перемещение строки в таблице вниз.
        Определяем индекс текущей строки и столбца. Сравниваем текущий
        индекс строки. Если он меньше, чем текущий индекс - 1, создаем
        новую строку. Запускаем цикл в диапазоне количества столбцов, перемещаем
        содержимое вниз. После цикла удаляем строку, которую нужно было переместить.
        """
        row = self.ui.fileView.currentRow()
        column = self.ui.fileView.currentColumn()
        if row < self.ui.fileView.rowCount() - 1:
            self.ui.fileView.insertRow(row + 2)
            for i in range(self.ui.fileView.columnCount()):
                self.ui.fileView.setItem(row + 2, i, self.ui.fileView.takeItem(row, i))
                self.ui.fileView.setCurrentCell(row + 2, column)
            self.ui.fileView.removeRow(row)

    def move_up(self):
        """
        Перемещение строки в таблице вверх. Повторяем все те же операции, но сравниваем
        индекс текущей строки с нулем. Пока он больше, можем перемещать.
        """
        row = self.ui.fileView.currentRow()
        column = self.ui.fileView.currentColumn()
        if row > 0:
            self.ui.fileView.insertRow(row - 1)
            for i in range(self.ui.fileView.columnCount()):
                self.ui.fileView.setItem(row - 1, i, self.ui.fileView.takeItem(row + 1, i))
                self.ui.fileView.setCurrentCell(row - 1, column)
            self.ui.fileView.removeRow(row + 1)

    def del_line(self):
        """
        Удаление строки из таблицы.
        Определяем индекс строки, удаляем строку из таблицы
        по индексу.
        """
        row = self.ui.fileView.currentRow()
        self.ui.fileView.removeRow(row)

    def convert_html(self):
        """
        Функция для конвертации локальных страниц html
        в pdf. Устанавливаем значение прогресс-бара на 0.
        Создаем список из значений строк таблицы, то выбираем
        только те записи, которые имеют расширение html.
        Проверяем длину списка. Если 0, выходим из функции,
        в статус-бар пишем сообщение о том, что конвертировать нечего,
        то же сообщение выводим на экран.
        Если длина списка больше 0, устанавливаем максимальное значение
        статус-бара на длину списка, запускаем цикл по списку, с помощью функции
        enumerate получаем индекс элемента, устанавливаем значение статус-бара равным
        индексу + 1.
        Формируем пути к входным html и выходным pdf файлам.
        Запускаем конвертацию из файла, в качестве параметров передаем путь к файлу
        html и путь для выходного файла pdf. Обрабатываем исключение.
        После завершения цикла выводим в статус-бар сообщение о полной конвертации,
        то же сообщение выводим на экран. Запускаем функцию для чтения файлов из
        директории и переформирования содержимого таблицы на экране.
        :return: Выход из функции в случае ошибки.
        """
        self.ui.progressBar.setValue(0)
        files_html = [self.ui.fileView.item(path, 0).text() for path in range(0, self.ui.fileView.rowCount())
                      if self.ui.fileView.item(path, 0).text().endswith(".html")]

        if len(files_html) == 0:
            self.ui.statusbar.showMessage('Нечего конвертировать')
            linux_notify(f'"Нечего конвертировать"')
            return

        self.ui.progressBar.setMaximum(len(files_html))
        for num, file in enumerate(files_html):
            self.ui.progressBar.setValue(num + 1)
            html_name = os.path.join(self.path_file, file)
            pdf_name = os.path.join(self.path_file, f'{file[0:-4]}pdf')
            try:
                pdfkit.from_file(html_name, pdf_name)
            except OSError as ex:
                print(ex)
                continue
        self.ui.statusbar.showMessage('Все файлы конвертированы')
        linux_notify(f'"Все файлы конвертированы"')
        self.file_read()

    def merge_pdf(self):
        """
        Функция для слияния файлов pdf из таблицы. Слияние выполняется
        с помощью fitz. Создаем объект fitz, открываем каждый из файлов
        pdf и добавляем в созданные объект. После чего выполняем сохранение.
        Работает в разы быстрее аналогичной функции слияния у PyPDF2.
        :return: Выход из функции.
        """
        self.ui.progressBar.setValue(0)
        files_pdf = [self.ui.fileView.item(path, 0).text() for path in range(0, self.ui.fileView.rowCount())
                     if self.ui.fileView.item(path, 0).text().endswith("pdf")]
        if len(files_pdf) == 0 or len(files_pdf) == 1:
            self.ui.statusbar.showMessage('Нечего объединять')
            linux_notify(f'"Нечего объединять"')
            return
        result = fitz.open()
        path_dir = self.ui.pathEdt.text()
        save_path = QtWidgets.QFileDialog.getSaveFileName(None, "Имя для объединенного файла", path_dir, "*.pdf")
        path_name = f'{save_path[0]}{save_path[1].replace("*", "")}'
        if path_name == '':
            return

        self.ui.progressBar.setMaximum(len(files_pdf))
        for num, pdf in enumerate(files_pdf):
            file = os.path.join(self.path_file, pdf)
            self.ui.progressBar.setValue(num)
            with fitz.open(file) as mfile:
                result.insert_pdf(mfile)
        result.save(path_name)
        self.ui.progressBar.setValue(len(files_pdf))
        self.ui.statusbar.showMessage('Объединение завершено')
        linux_notify(f'"Объединение завершено"')
        self.file_read()

    def change_method(self):
        """
        Проверка метода сжатия на форме. В зависимости от этого
        делаем комбо-бокс со степенью сжатия активным или нет.
        :return: ВЫход из функции.
        """
        if self.ui.methodBox.currentText() == 'Ghost Script':
            self.ui.ratioBox.setEnabled(True)
        else:
            self.ui.ratioBox.setEnabled(False)

    def compress_pdf(self):
        """
        Обработка значения в комбо-боксе с методом сжатия.
        В зависимости от выбранного метода запускается необходимая
        для выполнения этого метода функци.
        :return: Выход из функции.
        """
        row = self.ui.fileView.currentRow()
        try:
            if not self.ui.fileView.item(row, 0).text().endswith(".pdf"):
                self.ui.statusbar.showMessage('Выбранный файл не PDF')
                linux_notify(f'"Выбранный файл не PDF"')
                return
        except AttributeError:
            self.ui.statusbar.showMessage('Файл не выбран')
            linux_notify(f'"Файл не выбран"')
            return

        path_file = os.path.join(self.path_file, self.ui.fileView.item(row, 0).text())
        out_file = f'{path_file[0:-4]}_compress.pdf'

        if self.ui.methodBox.currentText() == 'Ghost Script':
            self.ui.statusbar.showMessage('Сжатие PDF')
            linux_notify(f'"Сжатие PDF"')
            compress_perc = self.compress_gs(path_file, out_file)
            self.ui.statusbar.showMessage(compress_perc)
            linux_notify(f'"{compress_perc}"')
            self.file_read()
        elif self.ui.methodBox.currentText() == 'PyPDF2':
            self.ui.statusbar.showMessage('Сжатие PDF')
            linux_notify(f'"Сжатие PDF"')
            compress_perc = self.compress_file_pypdf2(path_file, out_file)
            self.ui.statusbar.showMessage(compress_perc)
            linux_notify(f'"{compress_perc}"')
            self.file_read()

    def compress_gs(self, path_file, out_file):
        """
        Сжатие файла pdf с помощью GhostScript.
        Параметр качества берется непосредственно из формы.
        Проверяется, существует ли файл, получаем путь к
        GhostScript. Считываем размер файла до сжатия.
        Запускаем команду для сжатия файла с передачей необходимых параметров.
        Считываем размер файла после сжатия, считаем коэффициент.
        :param path_file: путь к входному файлу.
        :param out_file: путь для выходного файла.
        :return: Возвращает степень сжатия файла в процентах.
        """
        power = int(self.ui.ratioBox.currentText())
        quality = {
            0: '/default',
            1: '/prepress',
            2: '/printer',
            3: '/ebook',
            4: '/screen'
        }

        if not os.path.isfile(path_file):
            return "Ошибка: неверный путь к файлу PDF"

        gs = shutil.which('gs')
        initial_size = os.path.getsize(path_file)
        subprocess.call([gs, '-sDEVICE=pdfwrite', '-dCompatibilityLevel=1.4',
                         f'-dPDFSETTINGS={quality[power]}',
                         '-dNOPAUSE', '-dQUIET', '-dBATCH',
                         f'-sOutputFile={out_file}',
                         path_file])
        final_size = os.path.getsize(out_file)
        ratio = 1 - (final_size / initial_size)
        return f"Compression by {ratio:.0%}"

    def compress_file_pypdf2(self, path_file, out_file):
        """
        Функция сжатия файла pdf с помощью библиотеки
        PyPDF2.
        Получаем размер файла до сжатия, создаем экземпляр
        класса писателя. Создаем экземпляр класса чтеца, с передачей
        пути к файлу для сжатия.
        Устанавливаем максимальное значение прогресс-бара.
        Запускаем цикл в диапазоне равном кол-ву страниц.
        Считываем каждую страницу, сжимаем, передаем писателю.
        Записываем массив данных страниц в файл.
        :param path_file: путь к входному файлу.
        :param out_file: путь для выходного файла.
        :return: Возвращает степень сжатия в процентах.
        """
        self.ui.progressBar.setValue(0)
        start_size = os.path.getsize(path_file)
        writer = PdfFileWriter()
        reader = PdfFileReader(path_file)
        self.ui.progressBar.setMaximum(reader.numPages)
        for num in range(0, reader.numPages):
            self.ui.progressBar.setValue(num+1)
            page = reader.getPage(num)
            page.compressContentStreams()
            writer.addPage(page)
        with open(out_file, 'wb') as file:
            writer.write(file)

        compress_size = os.path.getsize(out_file)
        compression_ratio = 1 - (compress_size / start_size)
        return f'Compression by: {compression_ratio:.0%}'

    def file_read(self):
        """
        Считываем содержимое полученной ранее директории с файлами.
        Добавляем в список все файлы pdf и html.
        Запускаем цикл по отсортированному списку, добавляем названия файлов
        таблицу, очищаем список файлов.
        """
        files = [x for x in Path(self.path_file).iterdir() if x.suffix == ".pdf" or x.suffix == ".html"]
        self.ui.fileView.setRowCount(0)

        if len(files) == 0:
            self.ui.statusbar.showMessage('Нет файлов для обработки')
            linux_notify('"Нет файлов для обработки"')
            return

        for fil in sorted(files):
            row_position = self.ui.fileView.rowCount()
            self.ui.fileView.insertRow(row_position)
            self.ui.fileView.setItem(row_position, 0, QtWidgets.QTableWidgetItem(str(fil.name)))
        files.clear()

    def folder_open(self):
        """
        Функция для получения пути к директории для открытия.
        Устанавливаем значения статус-бара, прогресс-бара.
        Получаем из диалога путь к директории, добавлем его в
        текстовую строку. Проверяем, не является ли полученное значение
        пустым, так как пользователь может передумать и нажать кнопку отмена.
        Если нет, активируем кнопки конвертации, слияния и сжатия.
        Запускаем функцию чтения содержимого директории.
        :return: Выход из функции.
        """
        self.ui.statusbar.showMessage('')
        self.ui.progressBar.setMaximum(100)
        self.ui.progressBar.setValue(0)

        self.path_file = QtWidgets.QFileDialog.getExistingDirectory(None, "Выбрать папку", ".")
        self.ui.pathEdt.setText(self.path_file)

        if self.path_file != '':
            self.file_read()
            self.ui.mergeBtn.setEnabled(True)
            self.ui.convertBtn.setEnabled(True)
            self.ui.compressBtn.setEnabled(True)
            self.ui.methodBox.setEnabled(True)
        else:
            self.ui.statusbar.showMessage('Не выбрана директория с файлами')
            linux_notify(f'"Не выбрана директория с файлами"')
            self.ui.pathEdt.setText('')
            self.ui.folderBtn.setFocus()
            return


def linux_notify(text):
    """
    Функция выводит сообщение с текстом на экран.
    Получаем текст, формируем команду, выполняем.
    :param text: Текстовая строка для сообщения.
    """
    command = f'''notify-send {text}'''
    system(command)


def update():
    """
    Проверка наличия необходимых пакетов, для работы скрипта.
    Проверяем, есть ли в переменных окружения пакет wkhtmltopdf.
    Необходим для конвертации html в pdf.
    Если данного пакета нет, выводим сообщение пользователю с просьбой
    ввести пароль sudo для установки данного пакета.
    Передаем параметры в команду Popen, а также введенных пароль.
    То же самое для GhostScript. Если в системе нет данного модуля, пользователю
    будет предложено его установить.
    """
    if not shutil.which('wkhtmltopdf'):
        sudo_password = QtWidgets.QInputDialog.getText(None, "Пароль для установки", "Внимание!\nНе установлен"
                                                                                     " wkhtmltopdf.\n"
                                                                                     "Для его установки введите "
                                                                                     "пароль sudo:",
                                                       QLineEdit.Normal)[0]
        command1 = 'apt install wkhtmltopdf'.split()
        p = subprocess.Popen(['sudo', '-S'] + command1, stdin=subprocess.PIPE, stderr=subprocess.PIPE,
                             universal_newlines=True)
        p.communicate(sudo_password + '\n')

    if not shutil.which('gs'):
        sudo_password = QtWidgets.QInputDialog.getText(None, "Пароль для установки", "Внимание!\nНе установлен"
                                                                                     " Ghost Script.\n"
                                                                                     "Для его установки введите "
                                                                                     "пароль sudo:",
                                                       QLineEdit.Normal)[0]
        command1 = 'apt install ghostscript'.split()
        p = subprocess.Popen(['sudo', '-S'] + command1, stdin=subprocess.PIPE, stderr=subprocess.PIPE,
                             universal_newlines=True)
        p.communicate(sudo_password + '\n')


if __name__ == '__main__':
    """
    Создается объект с Qt виджетами, запускается функция проверки пакетов,
    запускается Qt приложение.
    """
    app = QtWidgets.QApplication(sys.argv)
    update()
    myapp = MyWin()
    myapp.show()
    sys.exit(app.exec_())

Все файлы, которые мы создавали в первой части, а также код этой части я прикреплю во вложении.

Небольшое видео демонстрирующее работу:


Извините за качество, еще не приноровился записывать видео на Linux.

А на этом, пожалуй, все.

Спасибо за внимание. Надеюсь, данная информация была вам полезна
 

Вложения

  • ch_02.zip
    321,3 КБ · Просмотры: 90
  • Нравится
Реакции: dimit, coder0x и Strife

coder0x

Member
26.10.2022
5
0
BIT
0
Выглядит очень круто спасибо!
Жаль что в убунте гуи нельзя сделать чёрную тему как у кали(
 

Johan Van

Green Team
13.06.2020
352
661
BIT
160
Выглядит очень круто спасибо!
Жаль что в убунте гуи нельзя сделать чёрную тему как у кали(

Я думаю, что это можно сделать в свойствах формы, то есть задать ей изначальный цвет, чтобы он не зависел от цветовой схемы ОС. Но под рукой нет пока PyQt, потому точно сказать не могу.
 
  • Нравится
Реакции: coder0x

coder0x

Member
26.10.2022
5
0
BIT
0
Я думаю, что это можно сделать в свойствах формы, то есть задать ей изначальный цвет, чтобы он не зависел от цветовой схемы ОС. Но под рукой нет пока PyQt, потому точно сказать не могу.
Да, есть конечно такой возможность. Надо просто лазеть
 

coder0x

Member
26.10.2022
5
0
BIT
0
@Johan Van можно вопрос, а где вы научились изучении языка питон? Откуда у вас такие знании?))
 

Johan Van

Green Team
13.06.2020
352
661
BIT
160
@Johan Van можно вопрос, а где вы научились изучении языка питон? Откуда у вас такие знании?))

Да никаких особых знаний у меня нет. Курсы на Ютуб; разные видео - как сделать то или это; статьи в интернете. Ну и немного практики. У каждого свой способ обучения. Для меня подходит лучше всего обучение на практике. То есть, ставишь задачу и пытаешься ее решить. В процессе решения узнаешь что-то новое. В идеале, конечно, лучше всего пройти обучающие курсы. Но, пока что, они мне не особо по-карману. Да и не знаю... наверное я не особо привык платить за знания )) Раньше ведь было все несколько иначе и никого не волновали авторские права или лицензионность программы )
 
  • Нравится
Реакции: dream to perfection
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!