Рубрики
Без рубрики

WXPYPHON – Создание PDF Merger / Splitter Utility

Получите практические, реальные навыки Python на наших ресурсах и пути

Автор оригинала: Mike Driscoll.

Формат портативного документа (PDF) является известным форматом, популярим Adobe. Это предполагает создание документа, который должен оказывать то же самое по платформам.

У Python есть несколько библиотек, которые вы можете использовать для работы с PDFS:

  • ReportLab – Создание PDFS
  • Pypdf2 – манипулирование предсекусами PDF
  • PDFRW – также для манипулирования перед предкистанием PDFS, но также работает с ReportLab
  • Pdfminer – экстракты текста из pdfs

Есть несколько дополнительных пакетов, связанных с PDF Python, но эти четыре, вероятно, самые известные. Одна общая задача работы с PDFS – это необходимость объединения или объединения нескольких PDF в один PDF. Другая общая задача – это PDF и разделение одной или нескольких страниц в новый PDF.

Вы будете создавать графический интерфейс пользователя, который оба из этих задач используют Pypdf2 Отказ

В этом руководстве из моей книги создание приложений GUI с WxPython. Вы можете получить это здесь: создание приложений GUI с покупкой WXPYPHON теперь на LeanPub или Amazon

Полный код для этого урока можно найти на Github В Глава 10 папка.

Установка PYPDF2.

Pypdf2 Пакет может быть установлен с использованием Пип :

pip install pypdf2

Этот пакет довольно маленький, поэтому установка должна быть довольно быстрой.

Теперь, когда PypDF2 установлен, вы можете разработать свой интерфейс!

Проектирование интерфейса

Это приложение в основном две программы, содержащиеся в одном окне. Вам нужен способ отображения приложения слияния и разделение. Быть простой способ переключаться между ними было бы хорошо. Вы можете разработать свой собственный код обмен панели, или вы можете использовать один из множества ноутбуков WxPython.

Чтобы сохранить вещи проще, давайте использовать WX.NOTEBOOK для этого приложения.

Вот макет вкладки слияния:

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

Далее – макет вкладок расщепления:

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

Теперь давайте создадим это приложение!

Создание приложения

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

Вот что вы будете создавать:

  • Основной модуль
  • Модуль панели слияния
  • Модуль разделения панели

Давайте начнем с главного модуля!

Основной модуль

Как главная точка входа в вашей заявке, Главная Модуль имеет много ответственности. Он будет держать ваши другие панели и может быть концентратор между панелями, если им нужно будет общаться. Большую часть времени вы бы использовали для этого Pubsub.

Давайте пойдем вперед и напишите свою первую версию кода:

# main.py

import wx

from merge_panel import MergePanel
from split_panel import SplitPanel

Импорт для Главная Модуль хороший и короткий. Все, что вам нужно, это WX , MergePanel и Сплитпанель Отказ Последние два – это те, которые вы будете писать в ближайшее время.

Давайте пойдем вперед и напишите код Mainpanel хоть:

class MainPanel(wx.Panel):

    def __init__(self, parent):
        super().__init__(parent)

        main_sizer = wx.BoxSizer(wx.VERTICAL)
        notebook = wx.Notebook(self)
        merge_tab = MergePanel(notebook)
        notebook.AddPage(merge_tab, 'Merge PDFs')
        split_tab = SplitPanel(notebook)
        notebook.AddPage(split_tab, 'Split PDFs')
        main_sizer.Add(notebook, 1, wx.ALL | wx.EXPAND, 5)
        self.SetSizer(main_sizer)

Mainpanel где все действие есть. Здесь вы создали WX.NOTEBOOK и добавить MergePanel и Сплитпанель к этому. Затем вы добавляете ноутбук в Sizer, и вы закончите!

Вот код кадра, который вам нужно будет добавить:

class MainFrame(wx.Frame):

    def __init__(self):
        super().__init__(None, title='PDF Merger / Splitter',
                         size=(800, 600))
        self.panel = MainPanel(self)
        self.Show()

if __name__ == '__main__':
    app = wx.App(False)
    frame = MainFrame()
    app.MainLoop()

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

Теперь давайте перейдем и узнаем, как объединить PDFS!

Модуль merge_panel.

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

Давайте начнем!

# merge_panel.py

import os
import glob
import wx

from ObjectListView import ObjectListView, ColumnDefn
from PyPDF2 import PdfFileReader, PdfFileWriter

wildcard = "PDFs (*.pdf)|*.pdf"

Здесь вам нужно импортировать Python’s ОС Модуль для некоторых действий, связанных с путем и модуль GLACH для поиска. Вам также понадобится ObjectListView Для отображения информации PDF и PYPDF2 для объединения PDF-файлов.

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

Чтобы сделать UI более дружелюбнее, вы должны добавить поддержку перетаскивания:

class DropTarget(wx.FileDropTarget):

    def __init__(self, window):
        super().__init__()
        self.window = window

    def OnDropFiles(self, x, y, filenames):
        self.window.update_on_drop(filenames)
        return True

Вы можете распознать этот код из главы архиватора. На самом деле, это почти не изменилось. Вам все еще нужно подкласс wx.filedroptarget И пропустите его виджет, который вы хотите добавить поддержку перетаскивания. Вам также нужно переопределить OnDroopFile () Чтобы он позвонил методу, используя виджет, который вы передали. Для этого примера вы проходите в самом объекте панели.

Вам также необходимо создать класс для хранения информации о PDF. Этот класс будет использоваться вашим ObjectListView виджет.

Вот:

class Pdf:

    def __init__(self, pdf_path):
        self.full_path = pdf_path
        self.filename = os.path.basename(pdf_path)
        try:
            with open(pdf_path, 'rb') as f:
                pdf = PdfFileReader(f)
                number_of_pages = pdf.getNumPages()
        except:
            number_of_pages = 0
        self.number_of_pages = str(number_of_pages)

__init __ () это приятно и коротко в этот раз вокруг. Вы устанавливаете список PDF для проведения объектов PDF, которые будут объединены. Вы также создали и добавьте Droptarget на панель. Затем вы создаете main_sizer и вызовите create_ui (), что добавит все нужные вам виджеты.

Говоря о которых, давайте добавим create_ui () Далее:

def create_ui(self):
    btn_sizer = wx.BoxSizer()
    add_btn = wx.Button(self, label='Add')
    add_btn.Bind(wx.EVT_BUTTON, self.on_add_file)
    btn_sizer.Add(add_btn, 0, wx.ALL, 5)
    remove_btn = wx.Button(self, label='Remove')
    remove_btn.Bind(wx.EVT_BUTTON, self.on_remove)
    btn_sizer.Add(remove_btn, 0, wx.ALL, 5)
    self.main_sizer.Add(btn_sizer)

Метод create_ui () немного длинный. Код будет разбит, чтобы облегчить переваривание. Код выше добавит две кнопки:

  • Кнопка добавления файла
  • Кнопка удалить файл

Эти кнопки идут внутрь горизонтально ориентированного Sizer вдоль верхней части панели MERGE. Вы также связываете каждую из этих кнопок для своих собственных обработчиков событий.

Теперь давайте добавим виджет для отображения PDF для объединения:

    move_btn_sizer = wx.BoxSizer(wx.VERTICAL)
    row_sizer = wx.BoxSizer()

    self.pdf_olv = ObjectListView(
        self, style=wx.LC_REPORT | wx.SUNKEN_BORDER)
    self.pdf_olv.SetEmptyListMsg("No PDFs Loaded")
    self.update_pdfs()
    row_sizer.Add(self.pdf_olv, 1, wx.ALL | wx.EXPAND)

Здесь вы добавляете виджет ObjectListView в ROW_SIZER и позвоните update_pdfs () Чтобы обновить его, чтобы у него были метки столбцов.

Вам необходимо добавить поддержку для переупорядочения PDFS в виджете объектаListView, поэтому давайте добавим это следующее:

    move_up_btn = wx.Button(self, label='Up')
    move_up_btn.Bind(wx.EVT_BUTTON, self.on_move)
    move_btn_sizer.Add(move_up_btn, 0, wx.ALL, 5)
    move_down_btn = wx.Button(self, label='Down')
    move_down_btn.Bind(wx.EVT_BUTTON, self.on_move)
    move_btn_sizer.Add(move_down_btn, 0, wx.ALL, 5)
    row_sizer.Add(move_btn_sizer)
    self.main_sizer.Add(row_sizer, 1, wx.ALL | wx.EXPAND, 5)

Здесь вы добавляете еще две кнопки. Один для перемещения предметов вверх и один для движущихся предметов вниз. Эти две кнопки добавляются в вертикально ориентированную Sizer, move_btn_sizer , который в свою очередь добавляется к row_sizer. Наконец, row_sizer добавляется в main_sizer.

Вот последние несколько строк create_ui () Метод:

    merge_pdfs = wx.Button(self, label='Merge PDFs')
    merge_pdfs.Bind(wx.EVT_BUTTON, self.on_merge)
    self.main_sizer.Add(merge_pdfs, 0, wx.ALL | wx.CENTER, 5)

    self.SetSizer(self.main_sizer)

Эти последние четыре строки добавляют кнопку слияния и зацепили его к обработчику событий. Он также устанавливает Sizer панели на main_sizer Отказ

Теперь давайте создадим add_pdf ():

def add_pdf(self, path):
    self.pdfs.append(Pdf(path))

Вы будете вызывать этот метод с помощью PDF, который вы хотите объединиться с другим PDF. Этот метод создаст экземпляр класса PDF и добавить его в список PDFS.

Теперь вы готовы создать load_pdfs () :

def load_pdfs(self, path):
    pdf_paths = glob.glob(path + '/*.pdf')
    for path in pdf_paths:
        self.add_pdf(path)
    self.update_pdfs()

Этот метод занимает папку, а не файл. Затем он использует все, чтобы найти все PDF в этой папке. Вы будете зацикливаться по списку файлов, которые GOLP возвращаются и используют add_pdf () Чтобы добавить их в список PDFS. Затем вы вызовите update_pdfs (), которые будут обновлять пользовательский интерфейс с вновь добавленными файлами PDF.

Давайте выясним, что произойдет, когда вы нажмете кнопку Merge:

def on_merge(self, event):
    """
    TODO - Move this into a thread
    """
    objects = self.pdf_olv.GetObjects()
    if len(objects) < 2:
        with wx.MessageDialog(
            None,
            message='You need 2 or more files to merge!',
            caption='Error',
            style= wx.ICON_INFORMATION) as dlg:
            dlg.ShowModal()
        return
    with wx.FileDialog(
        self, message="Choose a file",
        defaultDir='~',
        defaultFile="",
        wildcard=wildcard,
        style=wx.FD_SAVE | wx.FD_CHANGE_DIR
        ) as dlg:
        if dlg.ShowModal() == wx.ID_OK:
            path = dlg.GetPath()
    if path:
        _, ext = os.path.splitext(path)
        if '.pdf' not in ext.lower():
            path = f'{path}.pdf'
        self.merge(path)

on_merge () Метод – это обработчик событий, который называется вашей кнопкой MERGE. DOCSTRING содержит сообщение TODO, чтобы напомнить вам, чтобы переместить код объединения в поток. Технически код, который вы будете двигаться, на самом деле в Слияние () Функция, но до тех пор, пока у вас есть какое-то напоминание, это не имеет большого значения.

Во всяком случае, вы используете Getobjects () Чтобы получить все PDF в виджете объектаListView. Затем вы проверяете, чтобы убедиться, что есть как минимум два файла PDF. Если нет, вы позволите пользователю узнать, что им нужно добавить больше PDF! В противном случае вы откроете wx.filedialog И у пользователя выбрали имя и местоположение для объединенного PDF.

Наконец, вы проверяете, добавил ли пользователь расширение .pdf и добавить его, если они не сделали. Тогда вы звоните Слияние () Отказ

Слияние () Способ удобно следующий метод, который вы должны создать:

def merge(self, output_path):
    pdf_writer = PdfFileWriter()

    objects = self.pdf_olv.GetObjects()

    for obj in objects:
        pdf_reader = PdfFileReader(obj.full_path)
        for page in range(pdf_reader.getNumPages()):
            pdf_writer.addPage(pdf_reader.getPage(page))

    with open(output_path, 'wb') as fh:
        pdf_writer.write(fh)

    with wx.MessageDialog(None, message='Save completed!',
                          caption='Save Finished',
                         style= wx.ICON_INFORMATION) as dlg:
        dlg.ShowModal()

Здесь вы создаете PdffileWriter () Объект для записи объединенного PDF. Затем вы получаете список объектов из виджета объектаListView, а не список PDFS. Это потому, что вы можете изменить порядок UI, поэтому список может быть не в правильном порядке. Следующим шагом является петлю на каждом из объектов и получите полный путь. Вы откроете путь, используя PDFFILEREAREADER и LOOP на всех страницах, добавляя каждую страницу на PDF_WRITER.

Как только все PDF и все их соответствующие страницы добавляются к PDF_WRITER, вы можете записать объединенный PDF на диск. Затем вы открываете WX.Messageedialog, который позволяет пользователю знать, что PDFS объединились.

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

Теперь давайте создадим on_add_file () :

def on_add_file(self, event):
    paths = None
    with wx.FileDialog(
        self, message="Choose a file",
        defaultDir='~',
        defaultFile="",
        wildcard=wildcard,
        style=wx.FD_OPEN | wx.FD_MULTIPLE
        ) as dlg:
        if dlg.ShowModal() == wx.ID_OK:
            paths = dlg.GetPaths()
    if paths:
        for path in paths:
            self.add_pdf(path)
        self.update_pdfs()

Этот код откроет WX.FileDialog и позволяет пользователю выбрать один или несколько файлов. Затем он возвращает их в виде списка путей. Затем вы можете включить эти пути и использовать add_path () Чтобы добавить их в список PDFS.

Теперь давайте выясним, как изменить порядок элементов в виджете объектаListView:

def on_move(self, event):
    btn = event.GetEventObject()
    label = btn.GetLabel()
    current_selection = self.pdf_olv.GetSelectedObject()
    data = self.pdf_olv.GetObjects()
    if current_selection:
        index = data.index(current_selection)
        new_index = self.get_new_index(
            label.lower(), index, data)
        data.insert(new_index, data.pop(index))
        self.pdfs = data
        self.update_pdfs()
        self.pdf_olv.Select(new_index)

Оба кнопки вверх и вниз связаны с обработчиком событий On_Move (). Вы можете получить доступ к какой кнопке под названием этот обработчик через event.geteventobject () , что вернет объект кнопки. Тогда вы можете получить ярлык кнопки. Далее вы должны получить текущее_selection и список объектов, которые назначены на данные. Теперь вы можете использовать атрибут индекса объекта списка, чтобы найти индекс текущего доступа.

После того, как у вас есть эта информация, вы передаете метку кнопок, индекс и список данных для Get_New_index () для расчета, какой направление следует пойти. После того, как у вас есть New_index, вы можете вставить его и удалить старый индекс, используя POP () метод. Затем сбросьте список PDFS в список данных, чтобы они совпадали. Последние два шага должны обновить виджет и повторно выберите элемент, который вы перемещали.

Давайте посмотрим, как получить этот новый индекс сейчас:

def get_new_index(self, direction, index, data):
    if direction == 'up':
        if index > 0:
            new_index = index - 1
        else:
            new_index = len(data)-1
    else:
        if index < len(data) - 1:
            new_index = index + 1
        else:
            new_index = 0
    return new_index

Здесь вы используете ярлык кнопки, направление, чтобы определить, какой способ переместить элемент. Если это «вверх», то вы проверяете, если индекс больше нуля и вычтите один. Если это равно нулю, вы принимаете всю длину списка и вычтите один, который должен переместить элемент обратно к другому концу списка.

Если вы пользователю ударили кнопку «вниз», вы проверяете, если индекс меньше длины данных минус один. В этом случае вы добавляете его к нему. В противном случае вы устанавливаете NEW_INDEX на ноль.

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

Следующее новое, чтобы узнать, как удалить элемент:

def on_remove(self, event):
    current_selection = self.pdf_olv.GetSelectedObject()
    if current_selection:
        index = self.pdfs.index(current_selection)
        self.pdfs.pop(index)
        self.pdf_olv.RemoveObject(current_selection)

Этот метод получит текущее_selection, POP () из списка PDFS, а затем использовать RemoveObject () Метод удалить его из виджета объектаListView.

Теперь давайте посмотрим на код, который называется при перетаскивании элементов на ваше приложение:

def update_on_drop(self, paths):
    for path in paths:
        _, ext = os.path.splitext(path)
        if os.path.isdir(path):
            self.load_pdfs(path)
        elif os.path.isfile(path) and ext.lower() == '.pdf':
            self.add_pdf(path)
            self.update_pdfs()

В этом случае вы цикле по пути и проверьте, является ли путь каталогом или файлом. Они также могут быть ссылками, но вы игнорируете их. Если путь – это каталог, вы звоните load_pdfs () с этим. В противном случае вы проверяете, есть ли файл расширение .PDF И если это так, вы называете add_pdf () с этим.

Последний метод создания IS update_pdfs () :

def update_pdfs(self):
    self.pdf_olv.SetColumns([
        ColumnDefn("PDF Name", "left", 200, "filename"),
        ColumnDefn("Full Path", "left", 250, "full_path"),
        ColumnDefn("Page Count", "left", 100, "number_of_pages")
    ])
    self.pdf_olv.SetObjects(self.pdfs)

Этот метод добавляет или сбрасывает имена столбцов и ширины. Он также добавляет список PDF через SetObjects () Отказ

Вот как выглядит панель слияния:

Теперь вы готовы создать Split_Panel!

Модуль Split_Panel

Split_panel Модуль немного проще, чем merge_panel было. Вам действительно нужна всего лишь пара текстовых элементов управления, некоторые этикетки и кнопка.

Давайте посмотрим, как все это заканчивается, выкладывая:

# split_panel.py

import os
import string
import wx

from PyPDF2 import PdfFileReader, PdfFileWriter

wildcard = "PDFs (*.pdf)|*.pdf"

Здесь вы импортируете Python’s ОС и строка модули. Вам также будет также нужно pypdf2, и переменная подстановки будет полезна для открытия и сохранения PDF.

Вам также понадобится Charvalidator Класс из главы калькулятора.

Он снова воспроизводится здесь:

class CharValidator(wx.Validator):
    '''
    Validates data as it is entered into the text controls.
    '''

    def __init__(self, flag):
        wx.Validator.__init__(self)
        self.flag = flag
        self.Bind(wx.EVT_CHAR, self.OnChar)

    def Clone(self):
        '''Required Validator method'''
        return CharValidator(self.flag)

    def Validate(self, win):
        return True

    def TransferToWindow(self):
        return True

    def TransferFromWindow(self):
        return True

    def OnChar(self, event):
        keycode = int(event.GetKeyCode())
        if keycode < 256:
            key = chr(keycode)
            if self.flag == 'no-alpha' and key in string.ascii_letters:
                return
            if self.flag == 'no-digit' and key in string.digits:
                return
        event.Skip()

Charvalidator Класс полезен для проверки того, что пользователь не входит в какие-либо буквы в текстовое управление. Вы будете использовать его для параметров разделения, что позволит пользователю выбрать, какие страницы они хотят разделить из ввода PDF.

Но прежде чем мы доберемся до этого, давайте создадим Сплитпанель :

class SplitPanel(wx.Panel):

    def __init__(self, parent):
        super().__init__(parent)
        font = wx.Font(12, wx.SWISS, wx.NORMAL, wx.NORMAL)
        main_sizer = wx.BoxSizer(wx.VERTICAL)

Первые несколько строк __init __ () Создать wx.font экземпляр и main_sizer Отказ

Вот следующие несколько строк __init __ () :

row_sizer = wx.BoxSizer()
lbl = wx.StaticText(self, label='Input PDF:')
lbl.SetFont(font)
row_sizer.Add(lbl, 0, wx.ALL | wx.CENTER, 5)
self.pdf_path = wx.TextCtrl(self, style=wx.TE_READONLY)
row_sizer.Add(self.pdf_path, 1, wx.EXPAND | wx.ALL, 5)
pdf_btn = wx.Button(self, label='Open PDF')
pdf_btn.Bind(wx.EVT_BUTTON, self.on_choose)
row_sizer.Add(pdf_btn, 0, wx.ALL, 5)
main_sizer.Add(row_sizer, 0, wx.EXPAND)

Этот бит кода добавляет ряд виджетов, которые содержатся внутри row_sizer. Здесь у вас есть хорошая этикетка, управление текстом для удержания входного PDF-тракта и кнопка «Открыть PDF». После добавления каждого из них на row_sizer вы добавите это Sizer на Main_sizer.

Теперь давайте добавим второй ряд виджетов:

msg = 'Type page numbers and/or page ranges separated by commas.' \
    ' For example: 1, 3 or 4-10. Note you cannot use both commas ' \
    'and dashes.'
directions_txt = wx.TextCtrl(
    self, value=msg,
    style=wx.TE_MULTILINE | wx.NO_BORDER)
directions_txt.SetFont(font)
directions_txt.Disable()
main_sizer.Add(directions_txt, 0, wx.ALL | wx.EXPAND, 5)

Эти строки кода создают многострочное управление текстовым управлением, которое не имеет границы. Он содержит направления использования для элемента управления PDF_SPLIT_OPTIONS и отображается под этим виджетом. Вы также Отключить () Directions_txt для предотвращения того, чтобы пользователю изменить указания.

Есть еще четыре строки, чтобы добавить в __init __ () :

split_btn = wx.Button(self, label='Split PDF')
split_btn.Bind(wx.EVT_BUTTON, self.on_split)
main_sizer.Add(split_btn, 0, wx.ALL | wx.CENTER, 5)
self.SetSizer(main_sizer)

Эти последние несколько строк добавит кнопку «Сплит PDF», связывайте его к обработчику события и добавьте кнопку в Sizer. Затем вы устанавливаете Sizer для панели.

Теперь, когда у вас написано сама UI, вам нужно начать писать другие методы:

def on_choose(self, event):
    path = None
    with wx.FileDialog(
        self, message="Choose a file",
        defaultDir='~',
        defaultFile="",
        wildcard=wildcard,
        style=wx.FD_OPEN | wx.FD_CHANGE_DIR
        ) as dlg:
        if dlg.ShowModal() == wx.ID_OK:
            path = dlg.GetPath()
    if path:
        self.pdf_path.SetValue(path)

on_choose () Обработчик событий вызывается, когда пользователь нажимает кнопку «Открыть PDF». Он загрузит wx.filedialog И если пользователь выбирает PDF, он установит текстовое управление PDF_PATH с помощью этого выбора пользователя.

Теперь давайте доберемся до мяса кода:

def on_split(self, event):
    output_path = None
    input_pdf = self.pdf_path.GetValue()
    split_options = self.pdf_split_options.GetValue()
    if not input_pdf:
        message='You must choose an input PDF!'
        self.show_message(message)
        return

Когда пользователь нажимает кнопку «Сплит PDF», вызывается on_Split (). Вы начнете, проверяя, выбрал ли пользователь PDF для разделения вообще. Если они этого не сделали, скажите им, чтобы сделать это, используя show_message () Метод и возврат.

Далее вы должны проверить, есть ли путь PDF, который пользователь выбрал все еще существует:

if not os.path.exists(input_pdf):
    message = f'Input PDF {input_pdf} does not exist!'
    self.show_message(message)
    return

Если PDF не существует, пусть пользователь узнает об ошибке и ничего не делай.

Теперь вам нужно проверить, поставит ли пользователь на Split_options:

if not split_options:
    message = 'You need to choose what page(s) to split off'
    self.show_message(message)
    return

Если пользователь не установил Split_Options, то ваше приложение не будет знать, какие страницы разделены. Так что расскажите пользователю.

Следующий чек – убедиться, что у пользователя нет как запятых, так и тире:

if ',' in split_options and '-' in split_options:
    message = 'You cannot have both commas and dashes in options'
    self.show_message(message)
    return

Вы можете теоретически поддерживать запятые, так и тире, но это сделает код более сложным. Если вы хотите добавить это, не стесняйтесь. На данный момент он не поддерживается.

Другой пункт, чтобы проверить, если есть более одной панели:

if split_options.count('-') > 1:
    message = 'You can only use one dash'
    self.show_message(message)
    return

Пользователи сложно, и дважды легко поднять кнопку, поэтому убедитесь, что пользователь узнать, что это не допускается.

Пользователь также может ввести одно отрицательное число:

if '-' in split_options:
    page_begin, page_end = split_options.split('-')
    if not page_begin or not page_end:
        message = 'Need both a beginning and ending page'
        self.show_message(message)
        return

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

Последний чек – убедиться, что пользователь ввел номер, а не только на черту или запятую:

if not any(char.isdigit() for char in split_options):
    message = 'You need to enter a page number to split off'
    self.show_message(message)
    return

Вы можете использовать Python любой встроенный для этого. Вы циклируете все символы в строке и спросите, если они цифрами. Если нет, то вы показываете сообщение для пользователя.

Теперь вы готовы создать сам файл разделения PDF:

with wx.FileDialog(
    self, message="Choose a file",
    defaultDir='~',
    defaultFile="",
    wildcard=wildcard,
    style=wx.FD_SAVE | wx.FD_CHANGE_DIR
    ) as dlg:
    if dlg.ShowModal() == wx.ID_OK:
        output_path = dlg.GetPath()

Этот код кода откроет версию сохранения wx.filedialog И позвольте пользователю выбрать имя и местоположение, чтобы сохранить разделение PDF.

Последний кусок кода для этой функции ниже:

if output_path:
    _, ext = os.path.splitext(output_path)
    if '.pdf' not in ext.lower():
        output_path = f'{output_path}.pdf'
    split_options = split_options.strip()
    self.split(input_pdf, output_path, split_options)

После того, как у вас есть Yound_Path, вы проверяете, чтобы пользователь добавил расширение .pdf. Если они этого не сделали, то вы добавите его для них. Затем вы распределите какое-либо ведущее или заканчивающее белое пространство в Split_options и Split Call ().

Теперь давайте создадим код, используемый для фактического разделения PDF:

def split(self, input_pdf, output_path, split_options):
    pdf = PdfFileReader(input_pdf)
    pdf_writer = PdfFileWriter()
    if ',' in split_options:
        pages = [page for page in split_options.split(',')
                 if page]
        for page in pages:
            pdf_writer.addPage(pdf.getPage(int(page)))
    elif '-' in split_options:
        page_begin, page_end = split_options.split('-')
        page_begin = int(page_begin)
        page_end = int(page_end)
        page_begin = self.get_actual_beginning_page(page_begin)

        for page in range(page_begin, page_end):
            pdf_writer.addPage(pdf.getPage(page))
    else:
        # User only wants a single page
        page_begin = int(split_options)
        page_begin = self.get_actual_beginning_page(page_begin)
        pdf_writer.addPage(pdf.getPage(page_begin))

Здесь вы создаете объект PDFFILEREADER, называемый PDF и объектом PDFFILEWRITER, называемый PDF_WRITER. Затем вы проверяете Split_Options, чтобы посмотреть, использовал ли пользователь запятыми или тире. Если пользователь пошел с разделенным запятым списком, то вы зацикливаете на страницах и добавляете их в писатель.

Если пользователь использовал тире, то вам нужно получить начальную страницу и окончательную страницу. Тогда вы называете get_actual_beginning_page () Способ сделать немного математики, потому что страница одна при использовании pypdf на самом деле ноль страницы. Как только у вас выясняется нормализованные числа, вы можете включить в систему на диапазоне страниц, используя функцию диапазона Python и добавлять страницы к объекту Writer.

Заявление о остальном используется только тогда, когда пользователь вводит один номер страницы, который они хотят отключить. Например, они могут просто захотеть из 20 страниц документа.

Последний шаг – написать новый PDF на диск:

# Write PDF to disk
with open(output_path, 'wb') as out:
    pdf_writer.write(out)

# Let user know that PDF is split
message = f'PDF split successfully to {output_path}'
self.show_message(message, caption='Split Finished',
                          style=wx.ICON_INFORMATION)

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

Давайте посмотрим на логику, вам нужно добавить в get_actual_beginning_page () Метод:

def get_actual_beginning_page(self, page_begin):
    if page_begin < 0 or page_begin == 1:
        page_begin = 0
    if page_begin > 1:
        # Take off by one error into account
        page_begin -= 1
    return page_begin

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

Теперь давайте создадим show_message () :

def show_message(self, message, caption='Error', style=wx.ICON_ERROR):
    with wx.MessageDialog(None, message=message,
                          caption=caption,
                          style=style) as dlg:
        dlg.ShowModal()

Это полезная функция для упаковки создания и разрушения WX.MessageLog. Это принимает следующие аргументы:

  • сообщение
  • подпись
  • Флаг стиля

Затем он использует Python с оператором для создания экземпляра диалогового окна и покажет его пользователю.

Вот что похожа на разделение панели, когда вы закончите кодирование:

Теперь вы готовы узнать о нитках и Wxpython!

Использование потоков в wxpython

Каждый инструментарий GUI обрабатывает потоки по-разному. Toolkit WXPYPHON GUI имеет три метода безопасных потоков, которые вы должны использовать, если вы хотите использовать потоки:

  • wx.callafter.
  • wx.calllater.
  • WX.PostEvent

Вы можете использовать эти методы для публикации информации от потока обратно в WXPYPHON.

Давайте обновим merge_panel Так что он использует темы!

Улучшение слияния PDF с нитями

Python поставляется с несколькими модулями, связанными с параллелизмом. Вы будете использовать модуль Threading здесь. Возьмите оригинальный код и скопируйте его в новую папку, называемую version_2_threaded Или обратитесь к предварительно сделанной папке в репозитории GitHub для этой главы.

Давайте начнем с обновления импорта в merge_panel :

# merge_panel.py

import os
import glob
import wx

from ObjectListView import ObjectListView, ColumnDefn
from pubsub import pub
from PyPDF2 import PdfFileReader, PdfFileWriter
from threading import Thread

wildcard = "PDFs (*.pdf)|*.pdf"

Единственные различия вот эта линия импорта: От резьбы импортной резьбы и добавление PUBSUB. Это дает нам возможность подкласса поток.

Давайте сделаем это следующим:

class MergeThread(Thread):

    def __init__(self, objects, output_path):
        super().__init__()
        self.objects = objects
        self.output_path = output_path
        self.start()

Mergethread Класс потребуется в списке объектов из виджета ObjectListView, а также в Output_Path. В конце __init __ () Вы говорите тему на Start () , который на самом деле вызывает Беги () метод выполнить.

Давайте переопределим это:

def run(self):
    pdf_writer = PdfFileWriter()
    page_count = 1
    
    for obj in self.objects:
        pdf_reader = PdfFileReader(obj.full_path)
        for page in range(pdf_reader.getNumPages()):
            pdf_writer.addPage(pdf_reader.getPage(page))
            wx.CallAfter(pub.sendMessage, 'update',
                         msg=page_count)
            page_count += 1

    # All pages are added, so write it to disk
    with open(self.output_path, 'wb') as fh:
        pdf_writer.write(fh)

    wx.CallAfter(pub.sendMessage, 'close')

Здесь вы создаете класс PDFFILEWRITER, а затем закручивать различные PDF, извлечь их страницы и добавляя их в объект писателя, как вы делали ранее. После добавления страницы вы используете WX.callafter для отправки сообщения с помощью PUBSUB обратно в тему GUI. В этом сообщении вы отправляете по текущей сумме страницы добавленных страниц. Это обновит диалоговое окно, которое имеет прогресс-бар на нем.

После того, как файл завершен, вы отправляете еще одно сообщение через Pubsub, чтобы сообщить диалоговое окно прогресса, чтобы закрыть.

Давайте создадим виджет прогресса:

class MergeGauge(wx.Gauge):

    def __init__(self, parent, range):
        super().__init__(parent, range=range)

        pub.subscribe(self.update_progress, "update")

    def update_progress(self, msg):
        self.SetValue(msg)

Чтобы создать виджет прогресса, вы можете использовать WXPYPHON WX.GAUGE. В указанном выше коде вы подклассьте виджет, и подписывайтесь на сообщение об обновлении. Всякий раз, когда он получает обновление, он изменят значение датчика соответственно.

Вам нужно будет поставить этот датчик в диалоговое окно, поэтому давайте создадим, следующее:

class MergeProgressDialog(wx.Dialog):

    def __init__(self, objects, path):
        super().__init__(None, title='Merging Progress')
        pub.subscribe(self.close, "close")

        sizer = wx.BoxSizer(wx.VERTICAL)
        lbl = wx.StaticText(self, label='Merging PDFS')
        sizer.Add(lbl, 0, wx.ALL | wx.CENTER, 5)
        total_page_count = sum([int(obj.number_of_pages)
                                for obj in objects])
        gauge = MergeGauge(self, total_page_count)
        sizer.Add(gauge, 0, wx.ALL | wx.EXPAND, 5)

        MergeThread(objects, output_path=path)
        self.SetSizer(sizer)

    def close(self):
        self.Close()

MergeProgressDialog Подпишитесь на диалог на сообщение «Закрыть». Он также добавляет метку и датчик/прогресс для себя. Затем он начинает Mergethread. Когда появляется сообщение «Закрытие», этот метод CLACK () называется, и диалоговое окно будет закрыто.

Другие изменения, которые вам нужно сделать, это в MergePanel Класс, в частности, метод Merge ():

def merge(self, output_path, objects):
    with MergeProgressDialog(objects, output_path) as dlg:
        dlg.ShowModal()

    with wx.MessageDialog(None, message='Save completed!',
                          caption='Save Finished',
                         style= wx.ICON_INFORMATION) as dlg:
        dlg.ShowModal()

Здесь вы обновляете метод, чтобы принять параметр объектов и создать MERGEPREGRESSDIALOG с этим и выходом_Path. Обратите внимание, что вам нужно будет изменить on_merge () пройти в списке объектов в дополнение к пути, чтобы сделать эту работу. Как только слияние закончено, диалоговое окно автоматически закрывается и уничтожит. Затем вы создадите один и тот же WXMESSAGEILOG, как и раньше, и показать, что пользователю позволить им знать, что объединенная PDF готов.

Вы можете использовать код здесь, чтобы обновить Split_panel Использовать нити тоже, если вы хотите. Это не должно происходить обязательно, если вы не думаете, что вы разделите десятки или сотни страниц. Большую часть времени, должно быть достаточно быстро, чтобы пользователь не заметил или не заботился при расщеплении PDF.

Обертывание

Разделение и слияние PDF могут быть сделаны с использованием Pypdf2 Отказ Вы также можете использовать PDFRW Если вы хотели. Есть много способов улучшить это приложение.

Вот несколько примеров:

  • Поставить расщепление в нить
  • Добавить кнопки панели инструментов
  • Добавить ярлыки клавиатуры
  • Добавить статус BAR

Однако вы многому научились в этой главе. Вы узнали, как объединиться и разделить PDF. Вы также узнали, как использовать темы с WxPython. Наконец, этот код продемонстрировал добавление некоторого обращения с ошибками на ваши входы, специально в Split_panel модуль.