#!/usr/bin/python3 # -*- coding: utf-8 -*- import gzip import traceback from os import getenv from pathlib import Path from sys import argv, stderr from typing import List from xml.etree import ElementTree from datetime import datetime from xml.etree.ElementTree import Element # Set to true if notify2 was successfully imported and initialized NOTIFY2 = False MSG_FALLBACK_STR = 'Messages will only be printed to stdout.' try: # DEPENDENCIES # -> https://pypi.org/project/notify2/ # -> https://pypi.org/project/dbus-python/ import notify2 notify2.init('Relative Xournal++ Background Paths') NOTIFY2 = True except ImportError as e: print(f'Optional dependency "notify2" is not available. {MSG_FALLBACK_STR}') except Exception as e: print(f'Error during import or initialization of notify2. ' f'Is the dependency "dbus-python" missing? {MSG_FALLBACK_STR}') def main(): modified_files = 0 xopp_files: List[Path] = [] try: xopp_files = parse_args() for xopp_file in xopp_files: modified_files += save_relative_paths(xopp_file=xopp_file) # Catch exception, print it to stderr and save as file except Exception as ex: traceback.print_exc() log_file = Path.home().joinpath('relative_paths.error') with open(file=log_file, mode='w') as file: traceback.print_exc(file=file) notify(summary='Exception', message=getattr(ex, 'message', str(ex))) exit(1) notify(summary='Success!', message=f'Modified {modified_files} out of {len(xopp_files)} files.') def parse_args() -> List[Path]: NAUTILUS_SELECTION = 'NAUTILUS_SCRIPT_SELECTED_FILE_PATHS' SCRIPT_NAME = 'relative-xopp-background' selected_files_str = getenv(NAUTILUS_SELECTION) if selected_files_str: selected_files = selected_files_str.split('\n') if len(selected_files[-1]) < 1: selected_files = selected_files[:-1] return [Path(file) for file in selected_files] if len(argv) < 2: usage_str = f'Usage:\n' \ f'\t{SCRIPT_NAME} [] [...]\n' \ f'\tenv {NAUTILUS_SELECTION}= {SCRIPT_NAME}' print(usage_str, file=stderr) raise Exception(usage_str) return [Path(file) for file in argv[1:]] def save_relative_paths(xopp_file: Path) -> int: """ Xournal++ stores XML content into gz compressed `.xopp` files. This method modifies the content a given `.xopp` file "xopp_file": In case there are PDF background files with an absolute path and they do not exist at that location but instead reside next to the "xopp_file" (located in the same directory), then their path is replaced with a relative one. :returns the number of modified .xopp files (0 or 1) """ xml_modified = False print(f'=== RELATIVE PATHS ===\nxopp_file: {xopp_file}') if not xopp_file.exists(): raise Exception(f'The given file does not exist: {xopp_file}') if not xopp_file.suffix == '.xopp': raise Exception(f'Expected a Xournal++ file ending in .xopp: {xopp_file.suffix}') # Open gz-compressed file with gzip.open(xopp_file, mode="rt", encoding="utf-8") as fi: xopp_str = fi.read() root: Element = ElementTree.fromstring(xopp_str) # # xml_file: Path = Path('2019_SozialeMedien_Umgang-mit-Social-Media.xopp.xml') # tree: ElementTree = ElementTree.parse(xml_file) # root: Element = tree.getroot() if not root.tag == 'xournal': raise Exception(f'Unexpected root element: {root.tag}') page: Element for page in root: if page.tag == 'page': background: Element for background in page: if background.tag == 'background' \ and 'type' in background.attrib \ and background.attrib['type'] == 'pdf' \ and 'domain' in background.attrib \ and background.attrib['domain'] == 'absolute' \ and 'filename' in background.attrib: print(f'type: {background.tag}, attrib: {background.attrib}') abs_pdf: Path = Path(background.attrib['filename']) if not abs_pdf.is_absolute(): continue # No action required as the path is already relative if abs_pdf.exists(): continue # No action required as the background pdf will be found by Xournal++ if len(abs_pdf.name) < 1: raise Exception(f'Expected the PDF file name to be non empty: {abs_pdf}') rel_pdf: Path = xopp_file.parent.joinpath(abs_pdf.name) if rel_pdf.exists(): # Replace absolute with relative path background.attrib['filename'] = abs_pdf.name xml_modified = True else: print("The PDF file was neither found at it's absolute path" f" nor in the same directory as this Xournal++ file: {abs_pdf}") if not xml_modified: print('No modifications.') return 0 print('The xml was modified!') # Current date as string including milliseconds date_str = datetime.today().strftime('%Y-%m-%d_%H-%M-%S_%f')[:-3] xopp_file: Path # Optional: Backup old .xopp file # xopp_file.rename(xopp_file.parent.joinpath(xopp_file.name + '.' + date_str + '.backup')) # Open in binary mode encoding = 'utf-8' fo = gzip.open(filename=xopp_file, mode='wb') # Write xml header # Default xml encoding: us-ascii # -> https://stackoverflow.com/a/28208190/6334421 fo.write(f'\n'.encode(encoding=encoding)) # Write xml content fo.write(ElementTree.tostring(root, encoding=encoding, method='xml')) fo.close() return 1 def notify(summary: str, message: str) -> None: """ If notify2 is initialized, the user is informed of the given message via a graphical dbus notification. If notify2 is not available, the message is instead printed to stdout. """ if NOTIFY2: n = notify2.Notification(summary=summary, message=message) n.show() else: print(f'=== MESSAGE ===\n{summary}\n{message}\n') if __name__ == '__main__': main()