mirror of
https://codeberg.org/privacy1st/xournalpp-relative-background
synced 2024-12-04 22:25:02 +01:00
216 lines
7.3 KiB
Python
Executable File
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()
|