add script

This commit is contained in:
Daniel Langbein 2021-09-22 22:10:35 +02:00
parent 4103ccf9f7
commit 3e5bff3d78
2 changed files with 190 additions and 0 deletions

View File

@ -0,0 +1,10 @@
# Relative Xournal++ background paths
Replace absolute paths of Xournal++ PDF backgrounds with relative ones.
## Installation
Place [relative-xopp-background](relative-xopp-background) in `~/.local/share/nautilus/scripts/relative-xopp-background` and make it executable.
If missing, install [notify2](https://pypi.org/project/notify2/) and (optionally)
[dbus-python](https://pypi.org/project/dbus-python/) with your package manager or with pip.

180
relative-xopp-background Executable file
View File

@ -0,0 +1,180 @@
#!/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. 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} <xopp-file-1> [<xopp-file-2>] [...]\n' \
f'\tenv {NAUTILUS_SELECTION}=<newline-delimited-list-of-xopp-files> {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}')
child1: Element
for child1 in root:
if child1.tag == 'page':
child2: Element
for child2 in child1:
if child2.tag == 'background' \
and 'type' in child2.attrib \
and child2.attrib['type'] == 'pdf' \
and 'domain' in child2.attrib \
and child2.attrib['domain'] == 'absolute' \
and 'filename' in child2.attrib:
print(f'type: {child2.tag}, attrib: {child2.attrib}')
abs_pdf: Path = Path(child2.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
child2.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'<?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()