From b527776e282e73d2b6180ee0b707d0ccdac558b7 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 7 Jan 2020 14:21:39 -0800 Subject: [PATCH] Start making settings widget --- dangerzone/__init__.py | 21 +---- dangerzone/common.py | 171 ++++++++++++++++++++++++++++++++++ dangerzone/main_window.py | 106 ++++++--------------- dangerzone/settings_widget.py | 79 ++++++++++++++++ dangerzone/tasks_widget.py | 88 +++++++++++++++++ share/container/Containerfile | 4 +- share/icon.png | Bin 0 -> 2873 bytes 7 files changed, 371 insertions(+), 98 deletions(-) create mode 100644 dangerzone/settings_widget.py create mode 100644 dangerzone/tasks_widget.py create mode 100644 share/icon.png diff --git a/dangerzone/__init__.py b/dangerzone/__init__.py index 28a8671..6e78734 100644 --- a/dangerzone/__init__.py +++ b/dangerzone/__init__.py @@ -25,22 +25,7 @@ def main(filename): # Common object common = Common() - # Main window - main_window = MainWindow(app, common) - - # If a filename wasn't passed in, get with with a dialog - if filename == "": - filename = QtWidgets.QFileDialog.getOpenFileName( - main_window, - "Open document", - filter="Documents (*.pdf *.docx *.doc *.xlsx *.xls *.pptx *.ppt *.odt *.fodt *.ods *.fods *.odp *.fodp *.odg *.fodg *.odf)", - ) - if filename[0] == "": - print("No document was not selected") - return - - filename = filename[0] - else: + if filename != "": # Validate filename filename = os.path.abspath(os.path.expanduser(filename)) try: @@ -51,6 +36,8 @@ def main(filename): except PermissionError: print("Permission denied") return + common.set_document_filename(filename) - main_window.start(filename) + # Main window + main_window = MainWindow(app, common) sys.exit(app.exec_()) diff --git a/dangerzone/common.py b/dangerzone/common.py index b6b8c1f..fc06ed8 100644 --- a/dangerzone/common.py +++ b/dangerzone/common.py @@ -2,6 +2,7 @@ import sys import os import inspect import tempfile +from PyQt5 import QtGui class Common(object): @@ -16,6 +17,176 @@ class Common(object): print(f"pixel_dir is: {self.pixel_dir.name}") print(f"safe_dir is: {self.safe_dir.name}") + self.document_filename = None + + self.fixed_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) + + self.ocr_languages = { + "Afrikaans": "ar", + "Amharic": "amh", + "Arabic": "ara", + "Assamese": "asm", + "Azerbaijani": "aze", + "Azerbaijani (Cyrillic)": "aze_cyrl", + "Belarusian": "bel", + "Bengali": "ben", + "Tibetan Standard": "bod", + "Bosnian": "bos", + "Breton": "bre", + "Bulgarian": "bul", + "Catalan": "cat", + "Cebuano": "ceb", + "Czech": "ces", + "Chinese - Simplified": "chi_sim", + "Chinese - Simplified (vertical)": "chi_sim_vert", + "Chinese - Traditional": "chi_tra", + "Chinese - Traditional (vertical)": "chi_tra_vert", + "Cherokee": "chr", + "Corsican": "cos", + "Welsh": "cym", + "Danish": "dan", + "German": "deu", + "Divehi": "div", + "Dzongkha": "dzo", + "Greek": "ell", + "English": "eng", + "English, Middle (1100-1500)": "enm", + "Esperanto": "epo", + "Estonian": "est", + "Basque": "eus", + "Faroese": "fao", + "Persian": "fas", + "Filipino": "fil", + "Finnish": "fin", + "French": "fra", + "Frankish": "frk", + "French, Middle (ca.1400-1600)": "frm", + "Frisian (Western)": "fry", + "Gaelic (Scots)": "gla", + "Irish": "gle", + "Galician": "glg", + "Gujarati": "guj", + "Hatian": "hat", + "Hebrew": "heb", + "Hindi": "hin", + "Croatian": "hrv", + "Hungarian": "hun", + "Armenian": "hye", + "Inuktitut": "iku", + "Indonesian": "ind", + "Icelandic": "isl", + "Italian": "ita", + "Italian - Old": "ita_old", + "Javanese": "jav", + "Japanese": "jpn", + "Japanese (vertical)": "jpn_vert", + "Kannada": "kan", + "Georgian": "kat", + "Old Georgian": "kat_old", + "Kazakh": "kaz", + "Khmer": "khm", + "Kyrgyz": "kir", + "Korean": "kor", + "Korean (vertical)": "kor_vert", + "Kurdish (Arabic)": "kur_ara", + "Lao": "lao", + "Latin": "lat", + "Latvian": "lav", + "Lithuanian": "lit", + "Luxembourgish": "ltz", + "Malayalam": "mal", + "Marathi": "mar", + "Macedonian": "mkd", + "Maltese": "mlt", + "Mongolian": "mon", + "Maori": "mri", + "Malay": "msa", + "Burmese": "mya", + "Nepali": "nep", + "Dutch": "nld", + "Norwegian": "nor", + "Occitan (post 1500)": "oci", + "Oriya": "ori", + "script and orientation": "osd", + "Punjabi": "pan", + "Polish": "pol", + "Portuguese": "por", + "Pashto": "pus", + "Quechua": "que", + "Romanian": "ron", + "Russian": "rus", + "Sanskrit": "san", + "Sinhala": "sin", + "Slovakian": "slk", + "Slovenian": "slv", + "Sindhi": "snd", + "Spanish": "spa", + "Spanish, Castilian - Old": "spa_old", + "Albanian": "sqi", + "Serbian": "srp", + "Serbian (Latin)": "srp_latn", + "Sundanese": "sun", + "Swahili": "swa", + "Swedish": "swe", + "Syriac": "syr", + "Tamil": "tam", + "Tatar": "tat", + "Telugu": "tel", + "Tajik": "tgk", + "Thai": "tha", + "Tigrinya": "tir", + "Tonga": "ton", + "Turkish": "tur", + "Uyghur": "uig", + "Ukrainian": "ukr", + "Urdu": "urd", + "Uzbek": "uzb", + "Uzbek (Cyrillic)": "uzb_cyrl", + "Vietnamese": "vie", + "Yiddish": "yid", + "Yoruba": "yor", + "Arabic script": "Arabic", + "Armenian script": "Armenian", + "Bengali script": "Bengali", + "Canadian Aboriginal script": "Canadian_Aboriginal", + "Cherokee script": "Cherokee", + "Cyrillic script": "Cyrillic", + "Devanagari script": "Devanagari", + "Ethiopic script": "Ethiopic", + "Fraktur script": "Fraktur", + "Georgian script": "Georgian", + "Greek script": "Greek", + "Gujarati script": "Gujarati", + "Gurmukhi script": "Gurmukhi", + "Han - Simplified script": "HanS", + "Han - Simplified (vertical) script": "HanS_vert", + "Han - Traditional script": "HanT", + "Han - Traditional (vertical) script": "HanT_vert", + "Hangul script": "Hangul", + "Hangul (vertical) script": "Hangul_vert", + "Hebrew script": "Hebrew", + "Japanese script": "Japanese", + "Japanese (vertical) script": "Japanese_vert", + "Kannada script": "Kannada", + "Khmer script": "Khmer", + "Lao script": "Lao", + "Latin script": "Latin", + "Malayalam script": "Malayalam", + "Myanmar script": "Myanmar", + "Oriya (Odia) script": "Oriya", + "Sinhala script": "Sinhala", + "Syriac script": "Syriac", + "Tamil script": "Tamil", + "Telugu script": "Telugu", + "Thaana script": "Thaana", + "Thai script": "Thai", + "Tibetan script": "Tibetan", + "Vietnamese script": "Vietnamese", + } + + def set_document_filename(self, filename): + self.document_filename = filename + def get_resource_path(self, filename): if getattr(sys, "dangerzone_dev", False): # Look for resources directory relative to python file diff --git a/dangerzone/main_window.py b/dangerzone/main_window.py index 89ad591..5d9bd25 100644 --- a/dangerzone/main_window.py +++ b/dangerzone/main_window.py @@ -2,7 +2,8 @@ import shutil import os from PyQt5 import QtCore, QtGui, QtWidgets -from .tasks import PullImageTask, BuildContainerTask, ConvertToPixels, ConvertToPDF +from .settings_widget import SettingsWidget +from .tasks_widget import TasksWidget class MainWindow(QtWidgets.QMainWindow): @@ -15,94 +16,43 @@ class MainWindow(QtWidgets.QMainWindow): self.setMinimumWidth(500) self.setMinimumHeight(400) - self.task_label = QtWidgets.QLabel() - self.task_label.setAlignment(QtCore.Qt.AlignCenter) - self.task_label.setStyleSheet("QLabel { font-weight: bold; font-size: 20px; }") - - font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) - self.task_details = QtWidgets.QLabel() - self.task_details.setStyleSheet( - "QLabel { background-color: #ffffff; font-size: 12px; padding: 10px; }" + # Header + logo = QtWidgets.QLabel() + logo.setPixmap( + QtGui.QPixmap.fromImage( + QtGui.QImage(self.common.get_resource_path("icon.png")) + ) ) - self.task_details.setFont(font) - self.task_details.setAlignment(QtCore.Qt.AlignTop) + header_label = QtWidgets.QLabel("dangerzone") + header_label.setFont(self.common.fixed_font) + header_label.setStyleSheet("QLabel { font-weight: bold; font-size: 50px; }") + header_layout = QtWidgets.QHBoxLayout() + header_layout.addStretch() + header_layout.addWidget(logo) + header_layout.addSpacing(10) + header_layout.addWidget(header_label) + header_layout.addStretch() - self.details_scrollarea = QtWidgets.QScrollArea() - self.details_scrollarea.setWidgetResizable(True) - self.details_scrollarea.setWidget(self.task_details) - self.details_scrollarea.verticalScrollBar().rangeChanged.connect( - self.scroll_to_bottom - ) + # Settings + self.settings_widget = SettingsWidget(self.common) + self.settings_widget.show() + # Tasks + self.tasks_widget = TasksWidget(self.common) + self.tasks_widget.hide() + + # Layout layout = QtWidgets.QVBoxLayout() - layout.addWidget(self.task_label) - layout.addWidget(self.details_scrollarea, stretch=1) + layout.addLayout(header_layout) + layout.addWidget(self.settings_widget, stretch=1) + layout.addWidget(self.tasks_widget, stretch=1) central_widget = QtWidgets.QWidget() central_widget.setLayout(layout) self.setCentralWidget(central_widget) - self.tasks = [PullImageTask, BuildContainerTask, ConvertToPixels, ConvertToPDF] - - def start(self, filename): - print(f"Input document: {filename}") - self.common.document_filename = filename self.show() - self.next_task() - - def next_task(self): - if len(self.tasks) == 0: - self.save_safe_pdf() - return - - self.task_details.setText("") - - self.current_task = self.tasks.pop(0)(self.common) - self.current_task.update_label.connect(self.update_label) - self.current_task.update_details.connect(self.update_details) - self.current_task.task_finished.connect(self.next_task) - self.current_task.task_failed.connect(self.task_failed) - self.current_task.start() - - def update_label(self, s): - self.task_label.setText(s) - - def update_details(self, s): - self.task_details.setText(s) - - def task_failed(self, err): - self.task_label.setText("Task failed :(") - self.task_details.setWordWrap(True) - self.task_details.setText( - f"Directory with pixel data: {self.common.pixel_dir.name}\n\n{err}" - ) - - def save_safe_pdf(self): - suggested_filename = ( - f"{os.path.splitext(self.common.document_filename)[0]}-safe.pdf" - ) - - filename = QtWidgets.QFileDialog.getSaveFileName( - self, "Save safe PDF", suggested_filename, filter="Documents (*.pdf)" - ) - if filename[0] == "": - print("Save file dialog canceled") - else: - source_filename = f"{self.common.safe_dir.name}/safe-output-compressed.pdf" - dest_filename = filename[0] - shutil.move(source_filename, dest_filename) - - # Clean up - self.common.pixel_dir.cleanup() - self.common.safe_dir.cleanup() - - # Quit - self.app.quit() - - def scroll_to_bottom(self, minimum, maximum): - self.details_scrollarea.verticalScrollBar().setValue(maximum) - def closeEvent(self, e): e.accept() self.app.quit() diff --git a/dangerzone/settings_widget.py b/dangerzone/settings_widget.py new file mode 100644 index 0000000..4724791 --- /dev/null +++ b/dangerzone/settings_widget.py @@ -0,0 +1,79 @@ +from PyQt5 import QtCore, QtGui, QtWidgets + + +class SettingsWidget(QtWidgets.QWidget): + def __init__(self, common): + super(SettingsWidget, self).__init__() + self.common = common + + # Dangerous document selection + self.dangerous_doc_label = QtWidgets.QLabel() + self.dangerous_doc_label.hide() + self.dangerous_doc_button = QtWidgets.QPushButton( + "Select dangerous document ..." + ) + self.dangerous_doc_button.setStyleSheet("QPushButton { font-weight: bold }") + + dangerous_doc_layout = QtWidgets.QHBoxLayout() + dangerous_doc_layout.addWidget(self.dangerous_doc_label) + dangerous_doc_layout.addWidget(self.dangerous_doc_button) + dangerous_doc_layout.addStretch() + + # Save safe version + self.save_checkbox = QtWidgets.QCheckBox("Save safe PDF") + self.save_lineedit = QtWidgets.QLineEdit() + self.save_lineedit.setReadOnly(True) + self.save_browse_button = QtWidgets.QPushButton("Save as...") + + save_layout = QtWidgets.QHBoxLayout() + save_layout.addWidget(self.save_checkbox) + save_layout.addWidget(self.save_lineedit) + save_layout.addWidget(self.save_browse_button) + save_layout.addStretch() + + # OCR document + self.ocr_checkbox = QtWidgets.QCheckBox("OCR document, language") + self.ocr_combobox = QtWidgets.QComboBox() + for k in self.common.ocr_languages: + self.ocr_combobox.addItem(k, QtCore.QVariant(self.common.ocr_languages[k])) + + ocr_layout = QtWidgets.QHBoxLayout() + ocr_layout.addWidget(self.ocr_checkbox) + ocr_layout.addWidget(self.ocr_combobox) + ocr_layout.addStretch() + + # Open safe document + self.open_checkbox = QtWidgets.QCheckBox("Open safe document") + self.open_combobox = QtWidgets.QComboBox() + + open_layout = QtWidgets.QHBoxLayout() + open_layout.addWidget(self.open_checkbox) + open_layout.addWidget(self.open_combobox) + open_layout.addStretch() + + # Update container + self.update_checkbox = QtWidgets.QCheckBox("Update container") + update_layout = QtWidgets.QHBoxLayout() + update_layout.addWidget(self.update_checkbox) + update_layout.addStretch() + + # Button + self.button_start = QtWidgets.QPushButton("Convert to Save Document") + self.button_start.setStyleSheet( + "QPushButton { font-size: 16px; font-weight: bold; padding: 10px; }" + ) + button_layout = QtWidgets.QHBoxLayout() + button_layout.addStretch() + button_layout.addWidget(self.button_start) + button_layout.addStretch() + + # Layout + layout = QtWidgets.QVBoxLayout() + layout.addLayout(dangerous_doc_layout) + layout.addLayout(save_layout) + layout.addLayout(ocr_layout) + layout.addLayout(open_layout) + layout.addLayout(update_layout) + layout.addLayout(button_layout) + layout.addStretch() + self.setLayout(layout) diff --git a/dangerzone/tasks_widget.py b/dangerzone/tasks_widget.py new file mode 100644 index 0000000..50ed9ea --- /dev/null +++ b/dangerzone/tasks_widget.py @@ -0,0 +1,88 @@ +from PyQt5 import QtCore, QtGui, QtWidgets + +from .tasks import PullImageTask, BuildContainerTask, ConvertToPixels, ConvertToPDF + + +class TasksWidget(QtWidgets.QWidget): + def __init__(self, common): + super(TasksWidget, self).__init__() + self.common = common + + self.task_label = QtWidgets.QLabel() + self.task_label.setAlignment(QtCore.Qt.AlignCenter) + self.task_label.setStyleSheet("QLabel { font-weight: bold; font-size: 20px; }") + + self.task_details = QtWidgets.QLabel() + self.task_details.setStyleSheet( + "QLabel { background-color: #ffffff; font-size: 12px; padding: 10px; }" + ) + self.task_details.setFont(self.common.fixed_font) + self.task_details.setAlignment(QtCore.Qt.AlignTop) + + self.details_scrollarea = QtWidgets.QScrollArea() + self.details_scrollarea.setWidgetResizable(True) + self.details_scrollarea.setWidget(self.task_details) + self.details_scrollarea.verticalScrollBar().rangeChanged.connect( + self.scroll_to_bottom + ) + + self.tasks = [PullImageTask, BuildContainerTask, ConvertToPixels, ConvertToPDF] + + def start(self, filename): + print(f"Input document: {filename}") + self.common.set_document_filename(filename) + self.show() + + self.next_task() + + def next_task(self): + if len(self.tasks) == 0: + self.save_safe_pdf() + return + + self.task_details.setText("") + + self.current_task = self.tasks.pop(0)(self.common) + self.current_task.update_label.connect(self.update_label) + self.current_task.update_details.connect(self.update_details) + self.current_task.task_finished.connect(self.next_task) + self.current_task.task_failed.connect(self.task_failed) + self.current_task.start() + + def update_label(self, s): + self.task_label.setText(s) + + def update_details(self, s): + self.task_details.setText(s) + + def task_failed(self, err): + self.task_label.setText("Task failed :(") + self.task_details.setWordWrap(True) + self.task_details.setText( + f"Directory with pixel data: {self.common.pixel_dir.name}\n\n{err}" + ) + + def save_safe_pdf(self): + suggested_filename = ( + f"{os.path.splitext(self.common.document_filename)[0]}-safe.pdf" + ) + + filename = QtWidgets.QFileDialog.getSaveFileName( + self, "Save safe PDF", suggested_filename, filter="Documents (*.pdf)" + ) + if filename[0] == "": + print("Save file dialog canceled") + else: + source_filename = f"{self.common.safe_dir.name}/safe-output-compressed.pdf" + dest_filename = filename[0] + shutil.move(source_filename, dest_filename) + + # Clean up + self.common.pixel_dir.cleanup() + self.common.safe_dir.cleanup() + + # Quit + self.app.quit() + + def scroll_to_bottom(self, minimum, maximum): + self.details_scrollarea.verticalScrollBar().setValue(maximum) diff --git a/share/container/Containerfile b/share/container/Containerfile index d36d79b..5b23f01 100644 --- a/share/container/Containerfile +++ b/share/container/Containerfile @@ -1,9 +1,7 @@ FROM ubuntu:18.04 RUN apt-get update && \ - apt-get install -y file poppler-utils imagemagick ghostscript tesseract-ocr libreoffice - -# TODO: when we support OCR in other languages, we need tesseract-ocr-all + apt-get install -y file poppler-utils imagemagick ghostscript tesseract-ocr tesseract-ocr-all libreoffice # Fix imagemagick policy to allow writing PDFs RUN sed -i '/rights="none" pattern="PDF"/c\' /etc/ImageMagick-6/policy.xml diff --git a/share/icon.png b/share/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4e8b27e657acbc23d0fef75a9bdcb65ac78ed05a GIT binary patch literal 2873 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 XUnt79yVeb&00000NkvXXu0mjfiaAxf literal 0 HcmV?d00001