diff --git a/.gitignore b/.gitignore index 45cd695..afc873c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +*.pdf + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/README.md b/README.md index 4afed9f..951cdb9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # PyPDF Builder -A cross-platform clone of [PDFTK Builder](http://angusj.com/pdftkb/) written in Python. Yes, Python! +A cross-platform utility to join, split, stamp, and rotate PDFs written in Python. Yes, Python! -The project's goal is a simple, end-user friendly GUI for the [PyPDF2](https://github.com/mstamy2/PyPDF2) package that can join, split, stamp/number, and rotate PDFs. +This project is inspired by Angus Johnson's [PDFTK Builder](http://angusj.com/pdftkb/). Its goal is a GUI that builds on [PyPDF2](https://github.com/mstamy2/PyPDF2) as well as other PDF related libraries and offers a unified and simple experience for end-users. ![](screenshot.png) @@ -12,10 +12,10 @@ Grab a copy of `virtualenv` or `virtualenvwrapper` and set up a virtual environm ``` git clone https://github.com/mrgnth/PyPDF-Builder.git -pip install -r requirements +pip install -r requirements.txt ``` -These instructions will get you a copy of the project up and running on your local machine for development ~~and testing~~ purposes. ~~See deployment for notes on how to deploy the project on a live system.~~ +These instructions will get you a copy of the project up and running on your local machine for development purposes. ### Prerequisites @@ -35,7 +35,16 @@ Python 3.6 was used in development… I haven't checked for compatibility with l ## Deployment -Distributable application for Windows, Linux and Mac OS using pyInstaller or similiar tool. This isn't all too clear yet. +Distributable application for Windows, Linux and Mac OS using [PyInstaller](https://pyinstaller.readthedocs.io/en/stable/): + +``` +pyinstaller --onefile --clean --windowed --add-data="mainwindow.ui:." \ + --hidden-import="pygubu.builder.ttkstdwidgets" \ + --hidden-import="pygubu.builder.widgets.dialog" \ + pypdfbuilder.py +``` + +Subsequent builds can be managed by editing the `.spec` file created by the first build and then simply running `pyinstaller pypdfbuilder.spec` to build the executable. Long term: Inclusion in Debian repos for direct installation on end-user systems. @@ -46,6 +55,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## Acknowledgments * [Matthew Stamy](https://github.com/mstamy2): Creator and current maintainer of the PyPDF2 Python package +* Angus Johnson: Creator of [PDFTK Builder](http://angusj.com/pdftkb/) ## To Do diff --git a/mainwindow.ui b/mainwindow.ui index 31938bd..f07e42f 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -723,7 +723,7 @@ rotatetab_save_as - Save As… + Save As … 0 10 @@ -763,7 +763,6 @@ - PyPDF v1.0 string:application_status_text 0 @@ -908,6 +907,7 @@ + disabled Use Poppler PDF Tools if available boolean:settings_use_poppler diff --git a/pypdfbuilder.py b/pypdfbuilder.py index 6bf69a2..80659b8 100644 --- a/pypdfbuilder.py +++ b/pypdfbuilder.py @@ -1,6 +1,7 @@ #!/usr/bin/python import os +import sys import appdirs import json from pathlib import Path as plPath @@ -9,12 +10,18 @@ from tkinter import filedialog from pygubu import Builder as pgBuilder -from pygubu.builder import ttkstdwidgets + +# if dist fails to start because it's missing these, uncomment these two imports +# import pygubu.builder.ttkstdwidgets +# import pygubu.builder.widgets.dialog from PyPDF2 import PdfFileMerger, PdfFileReader, PdfFileWriter -APPNAME = 'pypdfbuilder' -CURRENT_DIR = os.path.abspath(os.path.dirname(__file__)) +# check to see if we're running from stand-alone one-file executable: +if hasattr(sys, '_MEIPASS'): + CURRENT_DIR = sys._MEIPASS +else: + CURRENT_DIR = os.path.abspath(os.path.dirname(__file__)) USER_DIR = str(plPath.home()) CONFIG_DIR = appdirs.user_config_dir(APPNAME) DATA_DIR = appdirs.user_data_dir(APPNAME) @@ -185,7 +192,7 @@ def concat_filename(self, max_length=35): ''' basename = os.path.basename(self.__filepath) - concat_filename = f'{basename[0:35]}' + concat_filename = f'{basename[0:max_length]}' if len(basename) > max_length: concat_filename += '…' return concat_filename @@ -513,17 +520,17 @@ def __init__(self): self.builder = pgBuilder() self.builder.add_from_file(os.path.join(CURRENT_DIR, 'mainwindow.ui')) - self.mainwindow = self.builder.get_object('MainWindow') - self.settings_dialog = self.builder.get_object('SettingsDialog', self.mainwindow) - self.notebook = self.builder.get_object('AppNotebook') - self.tabs = { + self.__mainwindow = self.builder.get_object('MainWindow') + self.__settings_dialog = self.builder.get_object('SettingsDialog', self.__mainwindow) + self.__notebook = self.builder.get_object('AppNotebook') + self.__tabs = { 'join': self.builder.get_object('JoinFrame'), 'split': self.builder.get_object('SplitFrame'), 'bg': self.builder.get_object('BgFrame'), 'rotate': self.builder.get_object('RotateFrame'), } - self.mainmenu = self.builder.get_object('MainMenu') - self.mainwindow.config(menu=self.mainmenu) + self.__mainmenu = self.builder.get_object('MainMenu') + self.__mainwindow.config(menu=self.__mainmenu) self.__status_text_variable = self.builder.get_variable('application_status_text') self.__settings_use_poppler_variable = self.builder.get_variable('settings_use_poppler') self.status_text = None @@ -532,10 +539,12 @@ def __init__(self): self.user_data = UserData() self.settings_data = SettingsData() - self.jointab = JoinTabManager(self) - self.splittab = SplitTabManager(self) - self.bgtab = BgTabManager(self) - self.rotatetab = RotateTabManager(self) + self.__jointab = JoinTabManager(self) + self.__splittab = SplitTabManager(self) + self.__bgtab = BgTabManager(self) + self.__rotatetab = RotateTabManager(self) + + self.status_text = DEFAULT_STATUS @property def status_text(self): @@ -549,55 +558,55 @@ def status_text(self, val): def select_tab_join(self, *args, **kwargs): '''Gets called when menu item "View > Join Files" is selected. Pops appropriate tab into view.''' - self.notebook.select(self.tabs['join']) + self.__notebook.select(self.__tabs['join']) def select_tab_split(self, *args, **kwargs): '''Gets called when menu item "View > Split File" is selected. Pops appropriate tab into view.''' - self.notebook.select(self.tabs['split']) + self.__notebook.select(self.__tabs['split']) def select_tab_bg(self, *args, **kwargs): '''Gets called when menu item "View > Background/Stamp/Number" is selected. Pops appropriate tab into view.''' - self.notebook.select(self.tabs['bg']) + self.__notebook.select(self.__tabs['bg']) def select_tab_rotate(self, *args, **kwargs): '''Gets called when menu item "View > Rotate Pages" is selected. Pops appropriate tab into view.''' - self.notebook.select(self.tabs['rotate']) + self.__notebook.select(self.__tabs['rotate']) def jointab_add_file(self): - self.jointab.add_file() + self.__jointab.add_file() def jointab_on_file_select(self, event): - self.jointab.on_file_select(event) + self.__jointab.on_file_select(event) def jointab_enter_page_selection(self, event): - self.jointab.enter_page_selection(event) + self.__jointab.enter_page_selection(event) def jointab_save_as(self): - self.jointab.save_as() + self.__jointab.save_as() def jointab_move_up(self): - self.jointab.move_up() + self.__jointab.move_up() def jointab_move_down(self): - self.jointab.move_down() + self.__jointab.move_down() def jointab_remove(self): - self.jointab.remove_file() + self.__jointab.remove_file() def splittab_open_file(self): - self.splittab.open_file() + self.__splittab.open_file() def splittab_save_as(self): - self.splittab.save_as() + self.__splittab.save_as() def bgtab_choose_bg_option(self): - self.bgtab.choose_bg_option() + self.__bgtab.choose_bg_option() def bgtab_choose_stamp_option(self): - self.bgtab.choose_stamp_option() + self.__bgtab.choose_stamp_option() def bgtab_choose_number_option(self): ''' @@ -607,19 +616,19 @@ def bgtab_choose_number_option(self): pass def bgtab_choose_source_file(self): - self.bgtab.choose_source_file() + self.__bgtab.choose_source_file() def bgtab_choose_bg_file(self): - self.bgtab.choose_bg_file() + self.__bgtab.choose_bg_file() def bgtab_save_as(self): - self.bgtab.save_as() + self.__bgtab.save_as() def rotatetab_open_file(self): - self.rotatetab.open_file() + self.__rotatetab.open_file() def rotatetab_save_as(self): - self.rotatetab.save_as() + self.__rotatetab.save_as() def save_success(self, status_text=DEFAULT_STATUS): '''Gets called when a PDF file was processed successfully. Currently only @@ -633,12 +642,12 @@ def show_settings(self, *args, **kwargs): and all the settings management is handled there. Args and kwargs are included in method definition in case it is triggered by the keyboard shortcut, in which case `event` gets passed into the call.''' - self.settings_dialog.run() + self.__settings_dialog.run() self.__settings_use_poppler_variable.set(self.settings_data.use_poppler_tools) def close_settings(self, *args, **kwargs): self.settings_data.use_poppler_tools = self.__settings_use_poppler_variable.get() - self.settings_dialog.close() + self.__settings_dialog.close() def cancel_settings(self, *args, **kwargs): pass @@ -657,10 +666,10 @@ def get_file_dialog(self, func, widget_title='Choose File(s) …'): return f def quit(self, event=None): - self.mainwindow.quit() + self.__mainwindow.quit() def run(self): - self.mainwindow.mainloop() + self.__mainwindow.mainloop() if __name__ == '__main__': diff --git a/settings.py b/settings.py index 07bb2c9..feaf980 100644 --- a/settings.py +++ b/settings.py @@ -1,3 +1,6 @@ +APPNAME = 'pypdfbuilder' +APPVERSION = '0.9' + # Constants for indexing into the values stored in the TreeView (e.g. in the Join View) PDF_FILENAME = 0 @@ -12,4 +15,4 @@ JOIN_FILE_SUCCESS = 'Files joined successfully to {}!' ROTATE_FILE_SUCCESS = 'Pages in {} rotated successfully!' BG_FILE_SUCCESS = 'File saved successfully to {}!' -DEFAULT_STATUS = 'PyPDF Builder v1.0' +DEFAULT_STATUS = F'PyPDF Builder v{APPVERSION}'