xournalpp-relative-background/relative-xopp-background

216 lines
7.3 KiB
Python
Executable File

#!/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.')
NAUTILUS_SELECTION = 'NAUTILUS_SCRIPT_SELECTED_FILE_PATHS'
SCRIPT_NAME = 'relative-xopp-background'
def parse_args() -> List[Path]:
"""
:return: List of absolute paths to .xopp files
"""
if len(argv) < 2:
usage()
# get list of selected paths
selected_paths: List[Path] = []
selected_files_str = getenv(NAUTILUS_SELECTION)
if selected_files_str:
selected_files = selected_files_str.split('\n')
# if last filename is empty, remove it
if len(selected_files[-1]) < 1:
selected_files = selected_files[:-1]
selected_paths = [Path(file) for file in selected_files]
else:
selected_paths = [Path(file) for file in argv[1:]]
# for each selected path
# if it is a file, check if it ends with .xopp
# if it is a directory, recursively add all .xopp files
xopp_files: List[Path] = []
path: Path
for path in selected_paths:
if not path.exists():
notify(summary='Note', message=f'Skipped non existing path {path}')
continue
elif path.is_dir():
for child in path.glob(r'**/*.xopp'):
if child.is_file():
xopp_files.append(child.absolute())
else:
if path.suffix == '.xopp':
xopp_files.append(path.absolute())
return xopp_files
def usage():
usage_str = f'Usage:\n' \
f'\t{SCRIPT_NAME} <.xopp file or folder> [<.xopp file or folder>] [...]\n' \
f'\tenv {NAUTILUS_SELECTION}=<newline delimited list of .xopp files or folders> {SCRIPT_NAME}'
notify(summary='Usage', message=usage_str)
exit(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!')
xopp_file: Path
# Optional: Backup old .xopp file
# Current date as string including milliseconds
# date_str = datetime.today().strftime('%Y-%m-%d_%H-%M-%S_%f')[:-3]
# 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'<?xml version="1.0" encoding="{encoding}" standalone="no"?>\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()