diff --git a/default.nix b/default.nix deleted file mode 100644 index 172b408..0000000 --- a/default.nix +++ /dev/null @@ -1,9 +0,0 @@ -with import {}; -with pkgs.python3Packages; - -buildPythonPackage { - name = "xopp2py"; - src = ./.; - format = "pyproject"; # tell Nix to use pyproject.toml - propagatedBuildInputs = [ hatchling ]; -} \ No newline at end of file diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..2416a4a --- /dev/null +++ b/shell.nix @@ -0,0 +1,15 @@ +# Generates a shell from where all the dependencies can be found. + +{ pkgs ? import {} }: + +let + SOURCE_DIR = builtins.toString ./src; +in + pkgs.mkShell { + buildInputs = [ + pkgs.python3 + pkgs.python3Packages.jinja2 + ]; + + PYTHONPATH = SOURCE_DIR; + } \ No newline at end of file diff --git a/src/xopp2py/abstract_syntax.py b/src/xopp2py/abstract_syntax.py index c627c6b..168ac86 100644 --- a/src/xopp2py/abstract_syntax.py +++ b/src/xopp2py/abstract_syntax.py @@ -3,20 +3,14 @@ from dataclasses import dataclass from decimal import Decimal -@dataclass -class Background: - type: str # could be enum - color: str # could be class - style: str # could be enum - @dataclass class Stroke: - values: str # Just the XML text in between the tags. Meaning: stroke positions. + text: str # Just the XML text in between the tags. Meaning: stroke positions. attributes: dict[str,str] # just the XML attributes as encountered @dataclass class Text: - text: str + text: str # Just the XML text between the tags. Meaning: the text contents of the text element attributes: dict[str,str] # just the XML attributes as encountered @dataclass @@ -27,7 +21,11 @@ class Layer: class Page: width: Decimal height: Decimal - background: Background + + background_type: str # could be enum + background_color: str # could be class + background_style: str # could be enum + layers: list[Layer] @dataclass @@ -35,5 +33,5 @@ class XournalFile: creator: str # e.g., "Xournal++ 1.1.2" fileversion: int # e.g., 4 title: str # obscure feature - preview: bytes # PNG-encoded preview of the (first page) of the file + preview: bytes | None # PNG-encoded preview of the (first page) of the file pages: list[Page] \ No newline at end of file diff --git a/src/xopp2py/main.py b/src/xopp2py/main.py index 2a44cae..f11d87c 100644 --- a/src/xopp2py/main.py +++ b/src/xopp2py/main.py @@ -6,7 +6,20 @@ if __name__ == "__main__": argparser = argparse.ArgumentParser( description = "Python interface for Xournal++ (.xopp) files.") argparser.add_argument('filename') + argparser.add_argument('--print-oml', action='store_true', help="Convert to OML and print") + argparser.add_argument('--write-oml', metavar='FILE', nargs=1, help="Convert to OML and write to file") args = argparser.parse_args() # exits on error - from parser import parseFile - print(parseFile(args.filename)) + print(args) + + from xopp2py import parser + + asyntax = parser.parseFile(args.filename) + if args.print_oml: + from xopp2py_oml import oml_writer + import sys + oml_writer.writeOML(asyntax, args.filename, "my_xopp", sys.stdout) + elif args.write_oml != None: + from xopp2py_oml import oml_writer + with open(args.write_oml[0], 'wt') as f: + oml_writer.writeOML(asyntax, args.filename, "my_xopp", f) \ No newline at end of file diff --git a/src/xopp2py/parser.py b/src/xopp2py/parser.py index 6b9649c..f73711b 100644 --- a/src/xopp2py/parser.py +++ b/src/xopp2py/parser.py @@ -1,4 +1,4 @@ -import abstract_syntax +from xopp2py import abstract_syntax def parseFile(path) -> abstract_syntax.XournalFile: """Parse a .xopp file.""" @@ -14,7 +14,7 @@ def parseFile(path) -> abstract_syntax.XournalFile: elif element.tag == "stroke": elements.append( abstract_syntax.Stroke( - values=element.text, attributes=element.attrib)) + text=element.text, attributes=element.attrib)) else: raise Error("Unsupported tag:" + element.tag) elif event == "end": @@ -29,10 +29,9 @@ def parseFile(path) -> abstract_syntax.XournalFile: for (event, element) in context: if event == "start": if element.tag == "background": - background = abstract_syntax.Background( - type=element.get("type"), - color=element.get("color"), - style=element.get("plain")) + background_type = element.get("type") + background_color = element.get("color") + background_style = element.get("style") elif element.tag == "layer": layers.append(parseLayer(context)) else: @@ -42,7 +41,9 @@ def parseFile(path) -> abstract_syntax.XournalFile: return abstract_syntax.Page( width=width, height=height, - background=background, + background_type=background_type, + background_color=background_color, + background_style=background_style, layers=layers) def parseXournal(context): @@ -56,7 +57,10 @@ def parseFile(path) -> abstract_syntax.XournalFile: title = element.text elif element.tag == "preview": import base64 - preview = base64.b64decode(element.text) + if element.text != None: + preview = base64.b64decode(element.text) + else: + preview = None elif element.tag == "page": pages.append(parsePage(element, context)) else: @@ -70,6 +74,8 @@ def parseFile(path) -> abstract_syntax.XournalFile: pages=pages) import gzip + # with gzip.open(path, mode='rt') as f: + # print(f.read()) with gzip.open(path, mode='rt') as f: from xml.etree import ElementTree context = ElementTree.iterparse(f, events=["start", "end"]) diff --git a/src/xopp2py_oml/__init__.py b/src/xopp2py_oml/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/xopp2py_oml/oml_writer.py b/src/xopp2py_oml/oml_writer.py new file mode 100644 index 0000000..d13ad1d --- /dev/null +++ b/src/xopp2py_oml/oml_writer.py @@ -0,0 +1,18 @@ +from xopp2py import abstract_syntax +import io + +def writeOML(xournalFile: abstract_syntax.XournalFile, inputfile:str, namespace:str, ostream: io.TextIOBase): + import jinja2 + import os + import base64 + + environment = jinja2.Environment( + loader=jinja2.FileSystemLoader(os.path.dirname(__file__))) + + template = environment.get_template("oml_writer.template") + for piece in template.generate( + file=xournalFile, + toBase64=base64.b64encode, + inputfile=inputfile, + namespace=namespace): + ostream.write(piece) diff --git a/src/xopp2py_oml/oml_writer.template b/src/xopp2py_oml/oml_writer.template new file mode 100644 index 0000000..ca636a5 --- /dev/null +++ b/src/xopp2py_oml/oml_writer.template @@ -0,0 +1,68 @@ +{%- macro attributes(pageindex, page, layerindex, layer, elementindex, element) -%} +{%- for key,value in element.attributes.items() %} + ci p{{pageindex}}l{{layerindex}}e{{elementindex}}a{{loop.index}} : xournalpp:XMLAttribute [ + xournalpp:hasKey "{{key}}" + xournalpp:hasValue "{{value}}" + xournalpp:ofLayerElement p{{pageindex}}l{{layerindex}}e{{elementindex}} + object_diagram:inModel model + ] +{% endfor %} +{%- endmacro -%} + +{%- macro elements(pageindex, page, layerindex, layer) -%} +{% for el in layer.elements %} + ci p{{pageindex}}l{{layerindex}}e{{loop.index}} : xournalpp:{{el.__class__.__name__}} [ + xournalpp:hasText "{{el.text}}" + xournalpp:inLayer p{{pageindex}}l{{layerindex}} + object_diagram:inModel model + ] + {{ attributes(pageindex, page, layerindex, layer, loop.index, el) -}} +{% endfor %} +{%- endmacro -%} + +{%- macro layers(pageindex, page) -%} +{% for layer in page.layers %} + ci p{{pageindex}}l{{loop.index}} : xournalpp:Layer [ + xournalpp:inPage p{{pageindex}} + object_diagram:inModel model + ] + {{ elements(pageindex, page, loop.index, layer) -}} +{% endfor %} +{%- endmacro -%} + +{%- macro pages(file) -%} +{% for page in file.pages -%} + ci p{{loop.index}} : xournalpp:Page [ + xournalpp:hasWidth {{ page.width }} + xournalpp:hasHeight {{ page.height }} + xournalpp:hasBackgroundType "{{ page.background_type }}" + xournalpp:hasBackgroundColor "{{ page.background_color }}" + xournalpp:hasBackgroundStyle "{{ page.background_style }}" + xournalpp:inFile file + object_diagram:inModel model + ] + {{ layers(loop.index, page) -}} +{%- endfor %} +{%- endmacro -%} +// Warning: Generated code! Do not edit! +// Input file: '{{inputfile}}' +// Generator: https://msdl.uantwerpen.be/git/jexelmans/xopp2py +description as {{namespace}} { + + uses as xournalpp + uses as object_diagram + + ci model : xournalpp:Model [] + + ci file : xournalpp:File [ + xournalpp:hasCreator "{{ file.creator }}" + xournalpp:hasFileVersion {{ file.fileversion }} + xournalpp:hasTitle "{{ file.title }}" + {%- if file.preview != None %} + xournalpp:hasPreview "{{ toBase64(file.preview).decode('utf-8') }}" + {%- endif %} + object_diagram:inModel model + ] + + {{ pages(file) }} +} diff --git a/tests/data/SmallXournalFile.xopp b/tests/data/SmallXournalFile.xopp new file mode 100644 index 0000000..44e802e Binary files /dev/null and b/tests/data/SmallXournalFile.xopp differ diff --git a/tests/data/TwoHiddenLayers.xopp b/tests/data/TwoHiddenLayers.xopp new file mode 100644 index 0000000..4d17d26 Binary files /dev/null and b/tests/data/TwoHiddenLayers.xopp differ diff --git a/tests/test_xopp2py.py b/tests/test_xopp2py.py new file mode 100644 index 0000000..6ba45ef --- /dev/null +++ b/tests/test_xopp2py.py @@ -0,0 +1,17 @@ +from xopp2py import parser, abstract_syntax +from xopp2py_oml import oml_writer +import os + +DATADIR = os.path.join(os.path.dirname(__file__), "data") + +class DummyOutput: + def write(self, text: str): + pass + +def parse(filename): + asyntax = parser.parseFile(os.path.join(DATADIR, filename)) + oml_writer.writeOML(asyntax, filename, "my_xopp", DummyOutput()) + +# Just see if these files parse without throwing an exception :) +parse("SmallXournalFile.xopp") +parse("TwoHiddenLayers.xopp") \ No newline at end of file