diff --git a/dangerzone/__init__.py b/dangerzone/__init__.py index 902d49a..3a1253b 100644 --- a/dangerzone/__init__.py +++ b/dangerzone/__init__.py @@ -6,7 +6,7 @@ import platform import click import time -from .common import Common +from .global_common import GlobalCommon from .main_window import MainWindow from .docker_installer import ( is_docker_installed, @@ -45,46 +45,60 @@ def main(filename): app = Application() app.setQuitOnLastWindowClosed(False) - # Common object - common = Common(app) + # GlobalCommon object + global_common = GlobalCommon(app) # If we're using Linux and docker, see if we need to add the user to the docker group - if platform.system() == "Linux" and common.container_runtime == "/usr/bin/docker": - if not common.ensure_user_is_in_docker_group(): + if ( + platform.system() == "Linux" + and global_common.container_runtime == "/usr/bin/docker" + ): + if not global_common.ensure_user_is_in_docker_group(): print("Failed to add user to docker group") return # See if we need to install Docker... if (platform.system() == "Darwin" or platform.system() == "Windows") and ( - not is_docker_installed(common) or not is_docker_ready(common) + not is_docker_installed(global_common) or not is_docker_ready(global_common) ): print("Docker is either not installed or not running") - docker_installer = DockerInstaller(common) + docker_installer = DockerInstaller(global_common) docker_installer.start() return - # Main window - main_window = MainWindow(common) + windows = [] + + # Open a document in a window + def select_document(filename=None): + if len(windows) == 1 and windows[0].common.document_filename == None: + window = windows[0] + else: + window = MainWindow(global_common) + windows.append(window) + + if filename: + # Validate filename + filename = os.path.abspath(os.path.expanduser(filename)) + try: + open(filename, "rb") + except FileNotFoundError: + print("File not found") + return False + except PermissionError: + print("Permission denied") + return False + window.common.document_filename = filename + window.doc_selection_widget.document_selected.emit() - def select_document(filename): - # Validate filename - filename = os.path.abspath(os.path.expanduser(filename)) - try: - open(filename, "rb") - except FileNotFoundError: - print("File not found") - return False - except PermissionError: - print("Permission denied") - return False - common.set_document_filename(filename) - main_window.doc_selection_widget.document_selected.emit() return True - # If filename is passed as an argument, open it - if filename is not None: + # Open a new window if not filename is passed + if filename is None: + select_document() + else: + # If filename is passed as an argument, open it if not select_document(filename): - return False + return True # If we get a file open event, open it app.document_selected.connect(select_document) diff --git a/dangerzone/common.py b/dangerzone/common.py index 9a2e449..9b479c7 100644 --- a/dangerzone/common.py +++ b/dangerzone/common.py @@ -1,35 +1,13 @@ -import sys -import os -import inspect -import tempfile -import appdirs import platform -import subprocess -import shlex -from PyQt5 import QtCore, QtGui, QtWidgets - -if platform.system() == "Darwin": - import CoreServices - import LaunchServices - import plistlib - -elif platform.system() == "Linux": - import grp - import getpass - from xdg.DesktopEntry import DesktopEntry - -from .settings import Settings +import tempfile class Common(object): """ - The Common class is a singleton of shared functionality throughout the app + The Common class is a singleton of shared functionality throughout an open dangerzone window """ - def __init__(self, app): - # Qt app - self.app = app - + def __init__(self): # Temporary directory to store pixel data # Note in macOS, temp dirs must be in /tmp (or a few other paths) for Docker to mount them if platform.system() == "Windows": @@ -44,411 +22,6 @@ class Common(object): f"Temporary directories created, dangerous={self.pixel_dir.name}, safe={self.safe_dir.name}" ) - # Name of input file + # Name of input and out files self.document_filename = None - - # Name of output file self.save_filename = None - - # Preload font - self.fixed_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) - - # App data folder - self.appdata_path = appdirs.user_config_dir("dangerzone") - - # Container runtime - if platform.system() == "Darwin": - self.container_runtime = "/usr/local/bin/docker" - elif platform.system() == "Windows": - self.container_runtime = ( - "C:\\Program Files\\Docker\\Docker\\resources\\bin\\docker.exe" - ) - else: - # Linux - - # If this is fedora-like, use podman - if os.path.exists("/usr/bin/dnf"): - self.container_runtime = "/usr/bin/podman" - # Otherwise, use docker - else: - self.container_runtime = "/usr/bin/docker" - - # Preload list of PDF viewers on computer - self.pdf_viewers = self._find_pdf_viewers() - - # Languages supported by tesseract - self.ocr_languages = { - "Afrikaans": "ar", - "Albanian": "sqi", - "Amharic": "amh", - "Arabic": "ara", - "Arabic script": "Arabic", - "Armenian": "hye", - "Armenian script": "Armenian", - "Assamese": "asm", - "Azerbaijani": "aze", - "Azerbaijani (Cyrillic)": "aze_cyrl", - "Basque": "eus", - "Belarusian": "bel", - "Bengali": "ben", - "Bengali script": "Bengali", - "Bosnian": "bos", - "Breton": "bre", - "Bulgarian": "bul", - "Burmese": "mya", - "Canadian Aboriginal script": "Canadian_Aboriginal", - "Catalan": "cat", - "Cebuano": "ceb", - "Cherokee": "chr", - "Cherokee script": "Cherokee", - "Chinese - Simplified": "chi_sim", - "Chinese - Simplified (vertical)": "chi_sim_vert", - "Chinese - Traditional": "chi_tra", - "Chinese - Traditional (vertical)": "chi_tra_vert", - "Corsican": "cos", - "Croatian": "hrv", - "Cyrillic script": "Cyrillic", - "Czech": "ces", - "Danish": "dan", - "Devanagari script": "Devanagari", - "Divehi": "div", - "Dutch": "nld", - "Dzongkha": "dzo", - "English": "eng", - "English, Middle (1100-1500)": "enm", - "Esperanto": "epo", - "Estonian": "est", - "Ethiopic script": "Ethiopic", - "Faroese": "fao", - "Filipino": "fil", - "Finnish": "fin", - "Fraktur script": "Fraktur", - "Frankish": "frk", - "French": "fra", - "French, Middle (ca.1400-1600)": "frm", - "Frisian (Western)": "fry", - "Gaelic (Scots)": "gla", - "Galician": "glg", - "Georgian": "kat", - "Georgian script": "Georgian", - "German": "deu", - "Greek": "ell", - "Greek script": "Greek", - "Gujarati": "guj", - "Gujarati script": "Gujarati", - "Gurmukhi script": "Gurmukhi", - "Hangul script": "Hangul", - "Hangul (vertical) script": "Hangul_vert", - "Han - Simplified script": "HanS", - "Han - Simplified (vertical) script": "HanS_vert", - "Han - Traditional script": "HanT", - "Han - Traditional (vertical) script": "HanT_vert", - "Hatian": "hat", - "Hebrew": "heb", - "Hebrew script": "Hebrew", - "Hindi": "hin", - "Hungarian": "hun", - "Icelandic": "isl", - "Indonesian": "ind", - "Inuktitut": "iku", - "Irish": "gle", - "Italian": "ita", - "Italian - Old": "ita_old", - "Japanese": "jpn", - "Japanese script": "Japanese", - "Japanese (vertical)": "jpn_vert", - "Japanese (vertical) script": "Japanese_vert", - "Javanese": "jav", - "Kannada": "kan", - "Kannada script": "Kannada", - "Kazakh": "kaz", - "Khmer": "khm", - "Khmer script": "Khmer", - "Korean": "kor", - "Korean (vertical)": "kor_vert", - "Kurdish (Arabic)": "kur_ara", - "Kyrgyz": "kir", - "Lao": "lao", - "Lao script": "Lao", - "Latin": "lat", - "Latin script": "Latin", - "Latvian": "lav", - "Lithuanian": "lit", - "Luxembourgish": "ltz", - "Macedonian": "mkd", - "Malayalam": "mal", - "Malayalam script": "Malayalam", - "Malay": "msa", - "Maltese": "mlt", - "Maori": "mri", - "Marathi": "mar", - "Mongolian": "mon", - "Myanmar script": "Myanmar", - "Nepali": "nep", - "Norwegian": "nor", - "Occitan (post 1500)": "oci", - "Old Georgian": "kat_old", - "Oriya (Odia) script": "Oriya", - "Oriya": "ori", - "Pashto": "pus", - "Persian": "fas", - "Polish": "pol", - "Portuguese": "por", - "Punjabi": "pan", - "Quechua": "que", - "Romanian": "ron", - "Russian": "rus", - "Sanskrit": "san", - "script and orientation": "osd", - "Serbian (Latin)": "srp_latn", - "Serbian": "srp", - "Sindhi": "snd", - "Sinhala script": "Sinhala", - "Sinhala": "sin", - "Slovakian": "slk", - "Slovenian": "slv", - "Spanish, Castilian - Old": "spa_old", - "Spanish": "spa", - "Sundanese": "sun", - "Swahili": "swa", - "Swedish": "swe", - "Syriac script": "Syriac", - "Syriac": "syr", - "Tajik": "tgk", - "Tamil script": "Tamil", - "Tamil": "tam", - "Tatar": "tat", - "Telugu script": "Telugu", - "Telugu": "tel", - "Thaana script": "Thaana", - "Thai script": "Thai", - "Thai": "tha", - "Tibetan script": "Tibetan", - "Tibetan Standard": "bod", - "Tigrinya": "tir", - "Tonga": "ton", - "Turkish": "tur", - "Ukrainian": "ukr", - "Urdu": "urd", - "Uyghur": "uig", - "Uzbek (Cyrillic)": "uzb_cyrl", - "Uzbek": "uzb", - "Vietnamese script": "Vietnamese", - "Vietnamese": "vie", - "Welsh": "cym", - "Yiddish": "yid", - "Yoruba": "yor", - } - - # Load settings - self.settings = Settings(self) - - 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 - prefix = os.path.join( - os.path.dirname( - os.path.dirname( - os.path.abspath(inspect.getfile(inspect.currentframe())) - ) - ), - "share", - ) - else: - if platform.system() == "Darwin": - prefix = os.path.join( - os.path.dirname(os.path.dirname(sys.executable)), "Resources/share" - ) - elif platform.system() == "Linux": - prefix = os.path.join(sys.prefix, "share", "dangerzone") - else: - # Windows - prefix = os.path.join(os.path.dirname(sys.executable), "share") - - resource_path = os.path.join(prefix, filename) - return resource_path - - def get_window_icon(self): - if platform.system() == "Windows": - path = self.get_resource_path("dangerzone.ico") - else: - path = self.get_resource_path("logo.png") - return QtGui.QIcon(path) - - def open_pdf_viewer(self, filename): - if self.settings.get("open_app") in self.pdf_viewers: - if platform.system() == "Darwin": - # Get the PDF reader bundle command - bundle_identifier = self.pdf_viewers[self.settings.get("open_app")] - args = ["open", "-b", bundle_identifier, filename] - - # Run - print(f"Executing: {' '.join(args)}") - subprocess.run(args) - - elif platform.system() == "Linux": - # Get the PDF reader command - args = shlex.split(self.pdf_viewers[self.settings.get("open_app")]) - # %f, %F, %u, and %U are filenames or URLS -- so replace with the file to open - for i in range(len(args)): - if ( - args[i] == "%f" - or args[i] == "%F" - or args[i] == "%u" - or args[i] == "%U" - ): - args[i] = filename - - # Open as a background process - print(f"Executing: {' '.join(args)}") - subprocess.Popen(args) - - def _find_pdf_viewers(self): - pdf_viewers = {} - - if platform.system() == "Darwin": - # Get all installed apps that can open PDFs - bundle_identifiers = LaunchServices.LSCopyAllRoleHandlersForContentType( - "com.adobe.pdf", CoreServices.kLSRolesAll - ) - for bundle_identifier in bundle_identifiers: - # Get the filesystem path of the app - res = LaunchServices.LSCopyApplicationURLsForBundleIdentifier( - bundle_identifier, None - ) - if res[0] is None: - continue - app_url = res[0][0] - app_path = str(app_url.path()) - - # Load its plist file - plist_path = os.path.join(app_path, "Contents/Info.plist") - with open(plist_path, "rb") as f: - plist_data = f.read() - plist_dict = plistlib.loads(plist_data) - - if plist_dict["CFBundleName"] != "Dangerzone": - pdf_viewers[plist_dict["CFBundleName"]] = bundle_identifier - - elif platform.system() == "Linux": - # Find all .desktop files - for search_path in [ - "/usr/share/applications", - "/usr/local/share/applications", - os.path.expanduser("~/.local/share/applications"), - ]: - try: - for filename in os.listdir(search_path): - full_filename = os.path.join(search_path, filename) - if os.path.splitext(filename)[1] == ".desktop": - - # See which ones can open PDFs - desktop_entry = DesktopEntry(full_filename) - if ( - "application/pdf" in desktop_entry.getMimeTypes() - and desktop_entry.getName() != "dangerzone" - ): - pdf_viewers[ - desktop_entry.getName() - ] = desktop_entry.getExec() - - except FileNotFoundError: - pass - - return pdf_viewers - - def ensure_user_is_in_docker_group(self): - try: - groupinfo = grp.getgrnam("docker") - except: - # Ignore if group is not found - return True - - username = getpass.getuser() - if username not in groupinfo.gr_mem: - # User is not in docker group, so prompt about adding the user to the docker group - message = "Dangerzone requires Docker.

Click Ok to add your user to the 'docker' group. You will have to type your login password." - if Alert(self, message).launch(): - p = subprocess.run( - [ - "/usr/bin/pkexec", - "/usr/sbin/usermod", - "-a", - "-G", - "docker", - username, - ] - ) - if p.returncode == 0: - message = "Great! Now you must log out of your computer and log back in, and then you can use Dangerzone." - Alert(self, message).launch() - else: - message = "Failed to add your user to the 'docker' group, quitting." - Alert(self, message).launch() - - return False - - return True - - def get_subprocess_startupinfo(self): - if platform.system() == "Windows": - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - return startupinfo - else: - return None - - -class Alert(QtWidgets.QDialog): - def __init__(self, common, message): - super(Alert, self).__init__() - self.common = common - - self.setWindowTitle("dangerzone") - self.setWindowIcon(self.common.get_window_icon()) - self.setModal(True) - - flags = ( - QtCore.Qt.CustomizeWindowHint - | QtCore.Qt.WindowTitleHint - | QtCore.Qt.WindowSystemMenuHint - | QtCore.Qt.WindowCloseButtonHint - | QtCore.Qt.WindowStaysOnTopHint - ) - self.setWindowFlags(flags) - - logo = QtWidgets.QLabel() - logo.setPixmap( - QtGui.QPixmap.fromImage( - QtGui.QImage(self.common.get_resource_path("icon.png")) - ) - ) - - label = QtWidgets.QLabel() - label.setText(message) - label.setWordWrap(True) - - message_layout = QtWidgets.QHBoxLayout() - message_layout.addWidget(logo) - message_layout.addWidget(label) - - ok_button = QtWidgets.QPushButton("Ok") - ok_button.clicked.connect(self.accept) - cancel_button = QtWidgets.QPushButton("Cancel") - cancel_button.clicked.connect(self.reject) - - buttons_layout = QtWidgets.QHBoxLayout() - buttons_layout.addStretch() - buttons_layout.addWidget(ok_button) - buttons_layout.addWidget(cancel_button) - - layout = QtWidgets.QVBoxLayout() - layout.addLayout(message_layout) - layout.addLayout(buttons_layout) - self.setLayout(layout) - - def launch(self): - return self.exec_() == QtWidgets.QDialog.Accepted diff --git a/dangerzone/doc_selection_widget.py b/dangerzone/doc_selection_widget.py index 7559673..2c79191 100644 --- a/dangerzone/doc_selection_widget.py +++ b/dangerzone/doc_selection_widget.py @@ -39,5 +39,5 @@ class DocSelectionWidget(QtWidgets.QWidget): ) if filename[0] != "": filename = filename[0] - self.common.set_document_filename(filename) + self.common.document_filename = filename self.document_selected.emit() diff --git a/dangerzone/docker_installer.py b/dangerzone/docker_installer.py index f2401cd..641656e 100644 --- a/dangerzone/docker_installer.py +++ b/dangerzone/docker_installer.py @@ -9,49 +9,49 @@ import platform from PyQt5 import QtCore, QtGui, QtWidgets -def is_docker_installed(common): +def is_docker_installed(global_common): if platform.system() == "Darwin": # Does the docker binary exist? if os.path.isdir("/Applications/Docker.app") and os.path.exists( - common.container_runtime + global_common.container_runtime ): # Is it executable? - st = os.stat(common.container_runtime) + st = os.stat(global_common.container_runtime) return bool(st.st_mode & stat.S_IXOTH) if platform.system() == "Windows": - return os.path.exists(common.container_runtime) + return os.path.exists(global_common.container_runtime) return False -def is_docker_ready(common): +def is_docker_ready(global_common): # Run `docker ps` without an error try: subprocess.run( - [common.container_runtime, "ps"], + [global_common.container_runtime, "ps"], check=True, - startupinfo=common.get_subprocess_startupinfo(), + startupinfo=global_common.get_subprocess_startupinfo(), ) return True except subprocess.CalledProcessError: return False -def launch_docker_windows(common): +def launch_docker_windows(global_common): docker_desktop_path = "C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe" subprocess.Popen( - [docker_desktop_path], startupinfo=common.get_subprocess_startupinfo() + [docker_desktop_path], startupinfo=global_common.get_subprocess_startupinfo() ) class DockerInstaller(QtWidgets.QDialog): - def __init__(self, common): + def __init__(self, global_common): super(DockerInstaller, self).__init__() - self.common = common + self.global_common = global_common self.setWindowTitle("dangerzone") - self.setWindowIcon(self.common.get_window_icon()) + self.setWindowIcon(self.global_common.get_window_icon()) self.setMinimumHeight(170) label = QtWidgets.QLabel() diff --git a/dangerzone/global_common.py b/dangerzone/global_common.py new file mode 100644 index 0000000..9b9a2a6 --- /dev/null +++ b/dangerzone/global_common.py @@ -0,0 +1,451 @@ +import sys +import os +import inspect +import tempfile +import appdirs +import platform +import subprocess +import shlex +from PyQt5 import QtCore, QtGui, QtWidgets + +if platform.system() == "Darwin": + import CoreServices + import LaunchServices + import plistlib + +elif platform.system() == "Linux": + import grp + import getpass + from xdg.DesktopEntry import DesktopEntry + +from .settings import Settings + + +class GlobalCommon(object): + """ + The GlobalCommon class is a singleton of shared functionality throughout the app + """ + + def __init__(self, app): + # Qt app + self.app = app + + # Temporary directory to store pixel data + # Note in macOS, temp dirs must be in /tmp (or a few other paths) for Docker to mount them + if platform.system() == "Windows": + self.pixel_dir = tempfile.TemporaryDirectory(prefix="dangerzone-pixel-") + self.safe_dir = tempfile.TemporaryDirectory(prefix="dangerzone-safe-") + else: + self.pixel_dir = tempfile.TemporaryDirectory( + prefix="/tmp/dangerzone-pixel-" + ) + self.safe_dir = tempfile.TemporaryDirectory(prefix="/tmp/dangerzone-safe-") + print( + f"Temporary directories created, dangerous={self.pixel_dir.name}, safe={self.safe_dir.name}" + ) + + # Name of input file + self.document_filename = None + + # Name of output file + self.save_filename = None + + # Preload font + self.fixed_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) + + # App data folder + self.appdata_path = appdirs.user_config_dir("dangerzone") + + # Container runtime + if platform.system() == "Darwin": + self.container_runtime = "/usr/local/bin/docker" + elif platform.system() == "Windows": + self.container_runtime = ( + "C:\\Program Files\\Docker\\Docker\\resources\\bin\\docker.exe" + ) + else: + # Linux + + # If this is fedora-like, use podman + if os.path.exists("/usr/bin/dnf"): + self.container_runtime = "/usr/bin/podman" + # Otherwise, use docker + else: + self.container_runtime = "/usr/bin/docker" + + # Preload list of PDF viewers on computer + self.pdf_viewers = self._find_pdf_viewers() + + # Languages supported by tesseract + self.ocr_languages = { + "Afrikaans": "ar", + "Albanian": "sqi", + "Amharic": "amh", + "Arabic": "ara", + "Arabic script": "Arabic", + "Armenian": "hye", + "Armenian script": "Armenian", + "Assamese": "asm", + "Azerbaijani": "aze", + "Azerbaijani (Cyrillic)": "aze_cyrl", + "Basque": "eus", + "Belarusian": "bel", + "Bengali": "ben", + "Bengali script": "Bengali", + "Bosnian": "bos", + "Breton": "bre", + "Bulgarian": "bul", + "Burmese": "mya", + "Canadian Aboriginal script": "Canadian_Aboriginal", + "Catalan": "cat", + "Cebuano": "ceb", + "Cherokee": "chr", + "Cherokee script": "Cherokee", + "Chinese - Simplified": "chi_sim", + "Chinese - Simplified (vertical)": "chi_sim_vert", + "Chinese - Traditional": "chi_tra", + "Chinese - Traditional (vertical)": "chi_tra_vert", + "Corsican": "cos", + "Croatian": "hrv", + "Cyrillic script": "Cyrillic", + "Czech": "ces", + "Danish": "dan", + "Devanagari script": "Devanagari", + "Divehi": "div", + "Dutch": "nld", + "Dzongkha": "dzo", + "English": "eng", + "English, Middle (1100-1500)": "enm", + "Esperanto": "epo", + "Estonian": "est", + "Ethiopic script": "Ethiopic", + "Faroese": "fao", + "Filipino": "fil", + "Finnish": "fin", + "Fraktur script": "Fraktur", + "Frankish": "frk", + "French": "fra", + "French, Middle (ca.1400-1600)": "frm", + "Frisian (Western)": "fry", + "Gaelic (Scots)": "gla", + "Galician": "glg", + "Georgian": "kat", + "Georgian script": "Georgian", + "German": "deu", + "Greek": "ell", + "Greek script": "Greek", + "Gujarati": "guj", + "Gujarati script": "Gujarati", + "Gurmukhi script": "Gurmukhi", + "Hangul script": "Hangul", + "Hangul (vertical) script": "Hangul_vert", + "Han - Simplified script": "HanS", + "Han - Simplified (vertical) script": "HanS_vert", + "Han - Traditional script": "HanT", + "Han - Traditional (vertical) script": "HanT_vert", + "Hatian": "hat", + "Hebrew": "heb", + "Hebrew script": "Hebrew", + "Hindi": "hin", + "Hungarian": "hun", + "Icelandic": "isl", + "Indonesian": "ind", + "Inuktitut": "iku", + "Irish": "gle", + "Italian": "ita", + "Italian - Old": "ita_old", + "Japanese": "jpn", + "Japanese script": "Japanese", + "Japanese (vertical)": "jpn_vert", + "Japanese (vertical) script": "Japanese_vert", + "Javanese": "jav", + "Kannada": "kan", + "Kannada script": "Kannada", + "Kazakh": "kaz", + "Khmer": "khm", + "Khmer script": "Khmer", + "Korean": "kor", + "Korean (vertical)": "kor_vert", + "Kurdish (Arabic)": "kur_ara", + "Kyrgyz": "kir", + "Lao": "lao", + "Lao script": "Lao", + "Latin": "lat", + "Latin script": "Latin", + "Latvian": "lav", + "Lithuanian": "lit", + "Luxembourgish": "ltz", + "Macedonian": "mkd", + "Malayalam": "mal", + "Malayalam script": "Malayalam", + "Malay": "msa", + "Maltese": "mlt", + "Maori": "mri", + "Marathi": "mar", + "Mongolian": "mon", + "Myanmar script": "Myanmar", + "Nepali": "nep", + "Norwegian": "nor", + "Occitan (post 1500)": "oci", + "Old Georgian": "kat_old", + "Oriya (Odia) script": "Oriya", + "Oriya": "ori", + "Pashto": "pus", + "Persian": "fas", + "Polish": "pol", + "Portuguese": "por", + "Punjabi": "pan", + "Quechua": "que", + "Romanian": "ron", + "Russian": "rus", + "Sanskrit": "san", + "script and orientation": "osd", + "Serbian (Latin)": "srp_latn", + "Serbian": "srp", + "Sindhi": "snd", + "Sinhala script": "Sinhala", + "Sinhala": "sin", + "Slovakian": "slk", + "Slovenian": "slv", + "Spanish, Castilian - Old": "spa_old", + "Spanish": "spa", + "Sundanese": "sun", + "Swahili": "swa", + "Swedish": "swe", + "Syriac script": "Syriac", + "Syriac": "syr", + "Tajik": "tgk", + "Tamil script": "Tamil", + "Tamil": "tam", + "Tatar": "tat", + "Telugu script": "Telugu", + "Telugu": "tel", + "Thaana script": "Thaana", + "Thai script": "Thai", + "Thai": "tha", + "Tibetan script": "Tibetan", + "Tibetan Standard": "bod", + "Tigrinya": "tir", + "Tonga": "ton", + "Turkish": "tur", + "Ukrainian": "ukr", + "Urdu": "urd", + "Uyghur": "uig", + "Uzbek (Cyrillic)": "uzb_cyrl", + "Uzbek": "uzb", + "Vietnamese script": "Vietnamese", + "Vietnamese": "vie", + "Welsh": "cym", + "Yiddish": "yid", + "Yoruba": "yor", + } + + # Load settings + self.settings = Settings(self) + + def get_resource_path(self, filename): + if getattr(sys, "dangerzone_dev", False): + # Look for resources directory relative to python file + prefix = os.path.join( + os.path.dirname( + os.path.dirname( + os.path.abspath(inspect.getfile(inspect.currentframe())) + ) + ), + "share", + ) + else: + if platform.system() == "Darwin": + prefix = os.path.join( + os.path.dirname(os.path.dirname(sys.executable)), "Resources/share" + ) + elif platform.system() == "Linux": + prefix = os.path.join(sys.prefix, "share", "dangerzone") + else: + # Windows + prefix = os.path.join(os.path.dirname(sys.executable), "share") + + resource_path = os.path.join(prefix, filename) + return resource_path + + def get_window_icon(self): + if platform.system() == "Windows": + path = self.get_resource_path("dangerzone.ico") + else: + path = self.get_resource_path("logo.png") + return QtGui.QIcon(path) + + def open_pdf_viewer(self, filename): + if self.settings.get("open_app") in self.pdf_viewers: + if platform.system() == "Darwin": + # Get the PDF reader bundle command + bundle_identifier = self.pdf_viewers[self.settings.get("open_app")] + args = ["open", "-b", bundle_identifier, filename] + + # Run + print(f"Executing: {' '.join(args)}") + subprocess.run(args) + + elif platform.system() == "Linux": + # Get the PDF reader command + args = shlex.split(self.pdf_viewers[self.settings.get("open_app")]) + # %f, %F, %u, and %U are filenames or URLS -- so replace with the file to open + for i in range(len(args)): + if ( + args[i] == "%f" + or args[i] == "%F" + or args[i] == "%u" + or args[i] == "%U" + ): + args[i] = filename + + # Open as a background process + print(f"Executing: {' '.join(args)}") + subprocess.Popen(args) + + def _find_pdf_viewers(self): + pdf_viewers = {} + + if platform.system() == "Darwin": + # Get all installed apps that can open PDFs + bundle_identifiers = LaunchServices.LSCopyAllRoleHandlersForContentType( + "com.adobe.pdf", CoreServices.kLSRolesAll + ) + for bundle_identifier in bundle_identifiers: + # Get the filesystem path of the app + res = LaunchServices.LSCopyApplicationURLsForBundleIdentifier( + bundle_identifier, None + ) + if res[0] is None: + continue + app_url = res[0][0] + app_path = str(app_url.path()) + + # Load its plist file + plist_path = os.path.join(app_path, "Contents/Info.plist") + with open(plist_path, "rb") as f: + plist_data = f.read() + plist_dict = plistlib.loads(plist_data) + + if plist_dict["CFBundleName"] != "Dangerzone": + pdf_viewers[plist_dict["CFBundleName"]] = bundle_identifier + + elif platform.system() == "Linux": + # Find all .desktop files + for search_path in [ + "/usr/share/applications", + "/usr/local/share/applications", + os.path.expanduser("~/.local/share/applications"), + ]: + try: + for filename in os.listdir(search_path): + full_filename = os.path.join(search_path, filename) + if os.path.splitext(filename)[1] == ".desktop": + + # See which ones can open PDFs + desktop_entry = DesktopEntry(full_filename) + if ( + "application/pdf" in desktop_entry.getMimeTypes() + and desktop_entry.getName() != "dangerzone" + ): + pdf_viewers[ + desktop_entry.getName() + ] = desktop_entry.getExec() + + except FileNotFoundError: + pass + + return pdf_viewers + + def ensure_user_is_in_docker_group(self): + try: + groupinfo = grp.getgrnam("docker") + except: + # Ignore if group is not found + return True + + username = getpass.getuser() + if username not in groupinfo.gr_mem: + # User is not in docker group, so prompt about adding the user to the docker group + message = "Dangerzone requires Docker.

Click Ok to add your user to the 'docker' group. You will have to type your login password." + if Alert(self, message).launch(): + p = subprocess.run( + [ + "/usr/bin/pkexec", + "/usr/sbin/usermod", + "-a", + "-G", + "docker", + username, + ] + ) + if p.returncode == 0: + message = "Great! Now you must log out of your computer and log back in, and then you can use Dangerzone." + Alert(self, message).launch() + else: + message = "Failed to add your user to the 'docker' group, quitting." + Alert(self, message).launch() + + return False + + return True + + def get_subprocess_startupinfo(self): + if platform.system() == "Windows": + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + return startupinfo + else: + return None + + +class Alert(QtWidgets.QDialog): + def __init__(self, common, message): + super(Alert, self).__init__() + self.common = common + + self.setWindowTitle("dangerzone") + self.setWindowIcon(self.common.get_window_icon()) + self.setModal(True) + + flags = ( + QtCore.Qt.CustomizeWindowHint + | QtCore.Qt.WindowTitleHint + | QtCore.Qt.WindowSystemMenuHint + | QtCore.Qt.WindowCloseButtonHint + | QtCore.Qt.WindowStaysOnTopHint + ) + self.setWindowFlags(flags) + + logo = QtWidgets.QLabel() + logo.setPixmap( + QtGui.QPixmap.fromImage( + QtGui.QImage(self.common.get_resource_path("icon.png")) + ) + ) + + label = QtWidgets.QLabel() + label.setText(message) + label.setWordWrap(True) + + message_layout = QtWidgets.QHBoxLayout() + message_layout.addWidget(logo) + message_layout.addWidget(label) + + ok_button = QtWidgets.QPushButton("Ok") + ok_button.clicked.connect(self.accept) + cancel_button = QtWidgets.QPushButton("Cancel") + cancel_button.clicked.connect(self.reject) + + buttons_layout = QtWidgets.QHBoxLayout() + buttons_layout.addStretch() + buttons_layout.addWidget(ok_button) + buttons_layout.addWidget(cancel_button) + + layout = QtWidgets.QVBoxLayout() + layout.addLayout(message_layout) + layout.addLayout(buttons_layout) + self.setLayout(layout) + + def launch(self): + return self.exec_() == QtWidgets.QDialog.Accepted diff --git a/dangerzone/main_window.py b/dangerzone/main_window.py index 4d6e231..5becd69 100644 --- a/dangerzone/main_window.py +++ b/dangerzone/main_window.py @@ -1,19 +1,22 @@ import shutil import os +import platform from PyQt5 import QtCore, QtGui, QtWidgets from .doc_selection_widget import DocSelectionWidget from .settings_widget import SettingsWidget from .tasks_widget import TasksWidget +from .common import Common class MainWindow(QtWidgets.QMainWindow): - def __init__(self, common): + def __init__(self, global_common): super(MainWindow, self).__init__() - self.common = common + self.global_common = global_common + self.common = Common() self.setWindowTitle("dangerzone") - self.setWindowIcon(self.common.get_window_icon()) + self.setWindowIcon(self.global_common.get_window_icon()) self.setMinimumWidth(600) self.setMinimumHeight(400) @@ -22,11 +25,11 @@ class MainWindow(QtWidgets.QMainWindow): logo = QtWidgets.QLabel() logo.setPixmap( QtGui.QPixmap.fromImage( - QtGui.QImage(self.common.get_resource_path("icon.png")) + QtGui.QImage(self.global_common.get_resource_path("icon.png")) ) ) header_label = QtWidgets.QLabel("dangerzone") - header_label.setFont(self.common.fixed_font) + header_label.setFont(self.global_common.fixed_font) header_label.setStyleSheet("QLabel { font-weight: bold; font-size: 50px; }") header_layout = QtWidgets.QHBoxLayout() header_layout.addStretch() @@ -41,7 +44,7 @@ class MainWindow(QtWidgets.QMainWindow): self.doc_selection_widget.show() # Settings - self.settings_widget = SettingsWidget(self.common) + self.settings_widget = SettingsWidget(self.global_common, self.common) self.doc_selection_widget.document_selected.connect( self.settings_widget.document_selected ) @@ -49,7 +52,8 @@ class MainWindow(QtWidgets.QMainWindow): self.settings_widget.hide() # Tasks - self.tasks_widget = TasksWidget(self.common) + self.tasks_widget = TasksWidget(self.global_common, self.common) + self.tasks_widget.close_window.connect(self.close) self.doc_selection_widget.document_selected.connect( self.tasks_widget.document_selected ) @@ -79,4 +83,5 @@ class MainWindow(QtWidgets.QMainWindow): def closeEvent(self, e): e.accept() - self.common.app.quit() + if platform.system() != "Darwin": + self.global_common.app.quit() diff --git a/dangerzone/settings_widget.py b/dangerzone/settings_widget.py index 18f5c00..9c4bb13 100644 --- a/dangerzone/settings_widget.py +++ b/dangerzone/settings_widget.py @@ -7,8 +7,9 @@ from PyQt5 import QtCore, QtGui, QtWidgets class SettingsWidget(QtWidgets.QWidget): start_clicked = QtCore.pyqtSignal() - def __init__(self, common): + def __init__(self, global_common, common): super(SettingsWidget, self).__init__() + self.global_common = global_common self.common = common # Dangerous document label @@ -47,9 +48,9 @@ class SettingsWidget(QtWidgets.QWidget): ) self.open_checkbox.clicked.connect(self.update_ui) self.open_combobox = QtWidgets.QComboBox() - for k in self.common.pdf_viewers: + for k in self.global_common.pdf_viewers: self.open_combobox.addItem( - k, QtCore.QVariant(self.common.pdf_viewers[k]) + k, QtCore.QVariant(self.global_common.pdf_viewers[k]) ) open_layout = QtWidgets.QHBoxLayout() open_layout.addWidget(self.open_checkbox) @@ -59,8 +60,10 @@ class SettingsWidget(QtWidgets.QWidget): # 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])) + for k in self.global_common.ocr_languages: + self.ocr_combobox.addItem( + k, QtCore.QVariant(self.global_common.ocr_languages[k]) + ) ocr_layout = QtWidgets.QHBoxLayout() ocr_layout.addWidget(self.ocr_checkbox) ocr_layout.addWidget(self.ocr_combobox) @@ -98,39 +101,43 @@ class SettingsWidget(QtWidgets.QWidget): self.setLayout(layout) # Load values from settings - if self.common.settings.get("save"): + if self.global_common.settings.get("save"): self.save_checkbox.setCheckState(QtCore.Qt.Checked) else: self.save_checkbox.setCheckState(QtCore.Qt.Unchecked) - if self.common.settings.get("ocr"): + if self.global_common.settings.get("ocr"): self.ocr_checkbox.setCheckState(QtCore.Qt.Checked) else: self.ocr_checkbox.setCheckState(QtCore.Qt.Unchecked) - index = self.ocr_combobox.findText(self.common.settings.get("ocr_language")) + index = self.ocr_combobox.findText( + self.global_common.settings.get("ocr_language") + ) if index != -1: self.ocr_combobox.setCurrentIndex(index) if platform.system() != "Windows": - if self.common.settings.get("open"): + if self.global_common.settings.get("open"): self.open_checkbox.setCheckState(QtCore.Qt.Checked) else: self.open_checkbox.setCheckState(QtCore.Qt.Unchecked) - index = self.open_combobox.findText(self.common.settings.get("open_app")) + index = self.open_combobox.findText( + self.global_common.settings.get("open_app") + ) if index != -1: self.open_combobox.setCurrentIndex(index) - if self.common.settings.get("update_container"): + if self.global_common.settings.get("update_container"): self.update_checkbox.setCheckState(QtCore.Qt.Checked) else: self.update_checkbox.setCheckState(QtCore.Qt.Unchecked) # Is update containers required? output = subprocess.check_output( - [self.common.container_runtime, "image", "ls", "dangerzone"], - startupinfo=self.common.get_subprocess_startupinfo(), + [self.global_common.container_runtime, "image", "ls", "dangerzone"], + startupinfo=self.global_common.get_subprocess_startupinfo(), ) if b"dangerzone" not in output: self.update_checkbox.setCheckState(QtCore.Qt.Checked) @@ -158,10 +165,9 @@ class SettingsWidget(QtWidgets.QWidget): ) # Update the save location - self.common.save_filename = ( - f"{os.path.splitext(self.common.document_filename)[0]}-safe.pdf" - ) - self.save_lineedit.setText(os.path.basename(self.common.save_filename)) + save_filename = f"{os.path.splitext(self.common.document_filename)[0]}-safe.pdf" + self.common.save_filename = save_filename + self.save_lineedit.setText(os.path.basename(save_filename)) def save_browse_button_clicked(self): filename = QtWidgets.QFileDialog.getSaveFileName( @@ -176,22 +182,24 @@ class SettingsWidget(QtWidgets.QWidget): def start_button_clicked(self): # Update settings - self.common.settings.set( + self.global_common.settings.set( "save", self.save_checkbox.checkState() == QtCore.Qt.Checked ) - self.common.settings.set( + self.global_common.settings.set( "ocr", self.ocr_checkbox.checkState() == QtCore.Qt.Checked ) - self.common.settings.set("ocr_language", self.ocr_combobox.currentText()) + self.global_common.settings.set("ocr_language", self.ocr_combobox.currentText()) if platform.system() != "Windows": - self.common.settings.set( + self.global_common.settings.set( "open", self.open_checkbox.checkState() == QtCore.Qt.Checked ) - self.common.settings.set("open_app", self.open_combobox.currentText()) - self.common.settings.set( + self.global_common.settings.set( + "open_app", self.open_combobox.currentText() + ) + self.global_common.settings.set( "update_container", self.update_checkbox.checkState() == QtCore.Qt.Checked ) - self.common.settings.save() + self.global_common.settings.save() # Start! self.start_clicked.emit() diff --git a/dangerzone/tasks.py b/dangerzone/tasks.py index 11a093b..01a968f 100644 --- a/dangerzone/tasks.py +++ b/dangerzone/tasks.py @@ -16,7 +16,7 @@ class TaskBase(QtCore.QThread): super(TaskBase, self).__init__() def exec_container(self, args, watch="stdout"): - args = [self.common.container_runtime] + args + args = [self.global_common.container_runtime] + args args_str = " ".join(pipes.quote(s) for s in args) print() @@ -31,7 +31,7 @@ class TaskBase(QtCore.QThread): stderr=subprocess.PIPE, bufsize=1, universal_newlines=True, - startupinfo=self.common.get_subprocess_startupinfo(), + startupinfo=self.global_common.get_subprocess_startupinfo(), ) as p: if watch == "stdout": pipe = p.stdout @@ -53,14 +53,15 @@ class TaskBase(QtCore.QThread): class PullImageTask(TaskBase): - def __init__(self, common): + def __init__(self, global_common, common): super(PullImageTask, self).__init__() + self.global_common = global_common self.common = common def run(self): self.update_label.emit("Pulling container image") self.update_details.emit("") - args = ["pull", "ubuntu:20.04"] + args = ["pull", "debian:buster"] returncode, _ = self.exec_container(args, watch="stderr") if returncode != 0: @@ -71,12 +72,13 @@ class PullImageTask(TaskBase): class BuildContainerTask(TaskBase): - def __init__(self, common): + def __init__(self, global_common, common): super(BuildContainerTask, self).__init__() + self.global_common = global_common self.common = common def run(self): - container_path = self.common.get_resource_path("container") + container_path = self.global_common.get_resource_path("container") self.update_label.emit("Building container (this might take a long time)") self.update_details.emit("") args = ["build", "-t", "dangerzone", container_path] @@ -90,8 +92,9 @@ class BuildContainerTask(TaskBase): class ConvertToPixels(TaskBase): - def __init__(self, common): + def __init__(self, global_common, common): super(ConvertToPixels, self).__init__() + self.global_common = global_common self.common = common self.max_image_width = 10000 @@ -119,7 +122,11 @@ class ConvertToPixels(TaskBase): # Did we hit an error? for line in output.split("\n"): - if "failed:" in line or "The document format is not supported" in line: + if ( + "failed:" in line + or "The document format is not supported" in line + or "Error" in line + ): self.task_failed.emit(output) return @@ -183,8 +190,9 @@ class ConvertToPixels(TaskBase): class ConvertToPDF(TaskBase): - def __init__(self, common): + def __init__(self, global_common, common): super(ConvertToPDF, self).__init__() + self.global_common = global_common self.common = common def run(self): @@ -192,13 +200,13 @@ class ConvertToPDF(TaskBase): # Build environment variables list envs = [] - if self.common.settings.get("ocr"): + if self.global_common.settings.get("ocr"): envs += ["-e", "OCR=1"] else: envs += ["-e", "OCR=0"] envs += [ "-e", - f"OCR_LANGUAGE={self.common.ocr_languages[self.common.settings.get('ocr_language')]}", + f"OCR_LANGUAGE={self.global_common.ocr_languages[self.global_common.settings.get('ocr_language')]}", ] args = ( diff --git a/dangerzone/tasks_widget.py b/dangerzone/tasks_widget.py index 92ce455..b70363b 100644 --- a/dangerzone/tasks_widget.py +++ b/dangerzone/tasks_widget.py @@ -9,8 +9,11 @@ from .tasks import PullImageTask, BuildContainerTask, ConvertToPixels, ConvertTo class TasksWidget(QtWidgets.QWidget): - def __init__(self, common): + close_window = QtCore.pyqtSignal() + + def __init__(self, global_common, common): super(TasksWidget, self).__init__() + self.global_common = global_common self.common = common # Dangerous document label @@ -28,7 +31,7 @@ class TasksWidget(QtWidgets.QWidget): self.task_details.setStyleSheet( "QLabel { background-color: #ffffff; font-size: 12px; padding: 10px; }" ) - self.task_details.setFont(self.common.fixed_font) + self.task_details.setFont(self.global_common.fixed_font) self.task_details.setAlignment(QtCore.Qt.AlignTop) self.details_scrollarea = QtWidgets.QScrollArea() @@ -55,7 +58,7 @@ class TasksWidget(QtWidgets.QWidget): ) def start(self): - if self.common.settings.get("update_container"): + if self.global_common.settings.get("update_container"): self.tasks += [PullImageTask, BuildContainerTask] self.tasks += [ConvertToPixels, ConvertToPDF] self.next_task() @@ -67,7 +70,7 @@ class TasksWidget(QtWidgets.QWidget): self.task_details.setText("") - self.current_task = self.tasks.pop(0)(self.common) + self.current_task = self.tasks.pop(0)(self.global_common, 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) @@ -91,7 +94,7 @@ class TasksWidget(QtWidgets.QWidget): def all_done(self): # Save safe PDF source_filename = f"{self.common.safe_dir.name}/safe-output-compressed.pdf" - if self.common.settings.get("save"): + if self.global_common.settings.get("save"): dest_filename = self.common.save_filename else: # If not saving, then save it to a temp file instead @@ -107,15 +110,19 @@ class TasksWidget(QtWidgets.QWidget): ) # Open - if self.common.settings.get("open"): - self.common.open_pdf_viewer(dest_filename) + if self.global_common.settings.get("open"): + self.global_common.open_pdf_viewer(dest_filename) # Clean up self.common.pixel_dir.cleanup() self.common.safe_dir.cleanup() # Quit - self.common.app.quit() + if platform.system() == "Darwin": + # In macOS, just close the window + self.close_window.emit() + else: + self.global_common.app.quit() def scroll_to_bottom(self, minimum, maximum): self.details_scrollarea.verticalScrollBar().setValue(maximum) diff --git a/share/container b/share/container index 48fbf72..9de7f4c 160000 --- a/share/container +++ b/share/container @@ -1 +1 @@ -Subproject commit 48fbf72a24a3c7f5e3f36baf00d4fd137ae7ab22 +Subproject commit 9de7f4cd81e71df0dcea4c4f9a76bee6a27cb234