From ecd858251fefd4bd85c63dbc1b366238d01eb770 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 28 Jan 2021 00:21:39 +0100 Subject: [PATCH] Add code for writing ODT files --- nw/core/toodt.py | 206 ++++++++++++++++++++++++----- nw/gui/build.py | 17 ++- tests/test_core/test_core_toodt.py | 2 +- 3 files changed, 183 insertions(+), 42 deletions(-) diff --git a/nw/core/toodt.py b/nw/core/toodt.py index 7687cd1a..1a801c1e 100644 --- a/nw/core/toodt.py +++ b/nw/core/toodt.py @@ -30,6 +30,7 @@ import logging from lxml import etree from hashlib import sha256 from datetime import datetime +from zipfile import ZipFile from nw.core.tokenizer import Tokenizer from nw.constants import nwLabels, nwKeyWords @@ -45,6 +46,9 @@ XML_NS = { "fo" : "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0", } +X_MIME = "application/vnd.oasis.opendocument.text" +X_VERS = "1.2" + TAG_BR = "{%s}line-break" % XML_NS["text"] TAG_TAB = "{%s}tab" % XML_NS["text"] TAG_SPAN = "{%s}span" % XML_NS["text"] @@ -58,16 +62,23 @@ class ToOdt(Tokenizer): X_BRK = 0x08 X_TAB = 0x10 - def __init__(self, theProject, theParent): + def __init__(self, theProject, theParent, isFlat): Tokenizer.__init__(self, theProject, theParent) self.mainConf = nw.CONFIG - self._xRoot = None + self._isFlat = isFlat + + self._dFlat = None + self._dCont = None + self._dMeta = None + self._dStyl = None + + self._xMeta = None self._xStyl = None + self._xAuto = None self._xBody = None self._xText = None - self._xAuto = None self._mainPara = {} self._autoPara = {} @@ -149,25 +160,9 @@ class ToOdt(Tokenizer): def initDocument(self): """Initialises a new open document XML tree. """ - rAttr = { - _mkTag("office", "version") : "1.3", - _mkTag("office", "mimetype") : "application/vnd.oasis.opendocument.text", - } - self._xRoot = etree.Element(_mkTag("office", "document"), attrib=rAttr, nsmap=XML_NS) - self._xMeta = etree.SubElement(self._xRoot, _mkTag("office", "meta")) - self._xFont = etree.SubElement(self._xRoot, _mkTag("office", "font-face-decls")) - self._xStyl = etree.SubElement(self._xRoot, _mkTag("office", "styles")) - self._xAuto = etree.SubElement(self._xRoot, _mkTag("office", "automatic-styles")) - self._xBody = etree.SubElement(self._xRoot, _mkTag("office", "body")) - self._xText = etree.SubElement(self._xBody, _mkTag("office", "text")) + # Initialise Variables + # ==================== - # Meta Data - xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "creation-date")) - xMeta.text = datetime.now().isoformat() - xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "generator")) - xMeta.text = f"novelWriter/{nw.__version__}" - - # Re-Init Variables self._fontFamily = self.textFont if len(self.textFont.split()) > 1: self._fontFamily = f"'{self.textFont}'" @@ -208,7 +203,69 @@ class ToOdt(Tokenizer): self._lineHeight = f"{round(100 * self.lineHeight):d}%" self._textAlign = "justify" if self.doJustify else "left" - # Add Styles + # Create Roots + # ============ + + tAttr = {} + tAttr[_mkTag("office", "version")] = X_VERS + + fAttr = {} + fAttr[_mkTag("style", "name")] = self.textFont + fAttr[_mkTag("style", "font-pitch")] = self._fontPitch + + if self._isFlat: + + # FODT File + # ========= + + tAttr[_mkTag("office", "mimetype")] = X_MIME + + tFlat = _mkTag("office", "document") + self._dFlat = etree.Element(tFlat, attrib=tAttr, nsmap=XML_NS) + + self._xMeta = etree.SubElement(self._dFlat, _mkTag("office", "meta")) + self._xFont = etree.SubElement(self._dFlat, _mkTag("office", "font-face-decls")) + self._xStyl = etree.SubElement(self._dFlat, _mkTag("office", "styles")) + self._xAuto = etree.SubElement(self._dFlat, _mkTag("office", "automatic-styles")) + self._xBody = etree.SubElement(self._dFlat, _mkTag("office", "body")) + + etree.SubElement(self._xFont, _mkTag("style", "font-face"), attrib=fAttr) + + else: + + # ODT File + # ======== + + tCont = _mkTag("office", "document-content") + tMeta = _mkTag("office", "document-meta") + tStyl = _mkTag("office", "document-styles") + + self._dCont = etree.Element(tCont, attrib=tAttr, nsmap=XML_NS) + self._xFnt1 = etree.SubElement(self._dCont, _mkTag("office", "font-face-decls")) + self._xAuto = etree.SubElement(self._dCont, _mkTag("office", "automatic-styles")) + self._xBody = etree.SubElement(self._dCont, _mkTag("office", "body")) + + self._dMeta = etree.Element(tMeta, attrib=tAttr, nsmap=XML_NS) + self._xMeta = etree.SubElement(self._dMeta, _mkTag("office", "meta")) + + self._dStyl = etree.Element(tStyl, attrib=tAttr, nsmap=XML_NS) + self._xFnt2 = etree.SubElement(self._dCont, _mkTag("office", "font-face-decls")) + self._xStyl = etree.SubElement(self._dStyl, _mkTag("office", "styles")) + + etree.SubElement(self._xFnt1, _mkTag("style", "font-face"), attrib=fAttr) + etree.SubElement(self._xFnt2, _mkTag("style", "font-face"), attrib=fAttr) + + # Finalise + # ======== + + self._xText = etree.SubElement(self._xBody, _mkTag("office", "text")) + + # Meta Data + xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "creation-date")) + xMeta.text = datetime.now().isoformat() + xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "generator")) + xMeta.text = f"novelWriter/{nw.__version__}" + self._defaultStyles() self._useableStyles() @@ -341,12 +398,97 @@ class ToOdt(Tokenizer): for styleName, styleObj in self._autoText.values(): styleObj.packXML(self._xAuto, styleName) - self.theResult = etree.tostring( - self._xRoot, - pretty_print = True, - encoding = "utf-8", - xml_declaration = True - ) + return + + def saveFlatXML(self, savePath): + """Save the data to an .fodt file. + """ + with open(savePath, mode="wb") as outFile: + outFile.write(etree.tostring( + self._dFlat, + pretty_print = True, + encoding = "utf-8", + xml_declaration = True + )) + return + + def saveOpenDocText(self, savePath): + """Save the data to an .odt file. + """ + rdfMap = {"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#"} + ns0Map = {"ns0": "http://docs.oasis-open.org/ns/office/1.2/meta/pkg#"} + + rdfRDF = "{%s}RDF" % rdfMap["rdf"] + rdfDesc = "{%s}Description" % rdfMap["rdf"] + rdfAbout = "{%s}about" % rdfMap["rdf"] + rdfType = "{%s}type" % rdfMap["rdf"] + rdfRes = "{%s}resource" % rdfMap["rdf"] + ns0Part = "{%s}hasPart" % ns0Map["ns0"] + + stylRes = "http://docs.oasis-open.org/ns/office/1.2/meta/odf#StylesFile" + contRes = "http://docs.oasis-open.org/ns/office/1.2/meta/odf#ContentFile" + docuRes = "http://docs.oasis-open.org/ns/office/1.2/meta/pkg#Document" + + xMRdf = etree.Element(rdfRDF, nsmap=rdfMap) + xDesc = etree.SubElement(xMRdf, rdfDesc, attrib={rdfAbout: "styles.xml"}) + _ = etree.SubElement(xDesc, rdfType, attrib={rdfRes: stylRes}) + xDesc = etree.SubElement(xMRdf, rdfDesc, attrib={rdfAbout: ""}) + _ = etree.SubElement(xDesc, ns0Part, attrib={rdfRes: "styles.xml"}, nsmap=ns0Map) + xDesc = etree.SubElement(xMRdf, rdfDesc, attrib={rdfAbout: "content.xml"}) + _ = etree.SubElement(xDesc, rdfType, attrib={rdfRes: contRes}) + xDesc = etree.SubElement(xMRdf, rdfDesc, attrib={rdfAbout: ""}) + _ = etree.SubElement(xDesc, ns0Part, attrib={rdfRes: "content.xml"}, nsmap=ns0Map) + xDesc = etree.SubElement(xMRdf, rdfDesc, attrib={rdfAbout: ""}) + _ = etree.SubElement(xDesc, rdfType, attrib={rdfRes: docuRes}) + + manMap = { + "manifest" : "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0", + "loext" : "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0", + } + manMani = "{%s}manifest" % manMap["manifest"] + manVers = "{%s}version" % manMap["manifest"] + manPath = "{%s}full-path" % manMap["manifest"] + manType = "{%s}media-type" % manMap["manifest"] + manFile = "{%s}file-entry" % manMap["manifest"] + typeXML = "text/xml" + typeRDF = "application/rdf+xml" + xMani = etree.Element(manMani, attrib={manVers: X_VERS}, nsmap=manMap) + etree.SubElement(xMani, manFile, attrib={manPath: "/", manVers: X_VERS, manType: X_MIME}) + etree.SubElement(xMani, manFile, attrib={manPath: "manifest.rdf", manType: typeRDF}) + etree.SubElement(xMani, manFile, attrib={manPath: "settings.xml", manType: typeXML}) + etree.SubElement(xMani, manFile, attrib={manPath: "content.xml", manType: typeXML}) + etree.SubElement(xMani, manFile, attrib={manPath: "meta.xml", manType: typeXML}) + etree.SubElement(xMani, manFile, attrib={manPath: "styles.xml", manType: typeXML}) + + setMap = { + "office" : "urn:oasis:names:tc:opendocument:xmlns:office:1.0", + "config" : "urn:oasis:names:tc:opendocument:xmlns:config:1.0", + } + offRoot = "{%s}document-settings" % setMap["office"] + offSett = "{%s}settings" % setMap["office"] + xSett = etree.Element(offRoot, nsmap=setMap) + etree.SubElement(xSett, offSett) + + with ZipFile(savePath, mode="w") as outFile: + outFile.writestr("mimetype", X_MIME) + outFile.writestr("META-INF/manifest.xml", etree.tostring( + xMani, pretty_print=True, encoding="utf-8", xml_declaration=True + )) + outFile.writestr("manifest.rdf", etree.tostring( + xMRdf, pretty_print=True, encoding="utf-8", xml_declaration=True + )) + outFile.writestr("settings.xml", etree.tostring( + xSett, pretty_print=True, encoding="utf-8", xml_declaration=True + )) + outFile.writestr("content.xml", etree.tostring( + self._dCont, pretty_print=True, encoding="utf-8", xml_declaration=True + )) + outFile.writestr("meta.xml", etree.tostring( + self._dMeta, pretty_print=True, encoding="utf-8", xml_declaration=True + )) + outFile.writestr("styles.xml", etree.tostring( + self._dStyl, pretty_print=True, encoding="utf-8", xml_declaration=True + )) return @@ -541,14 +683,6 @@ class ToOdt(Tokenizer): def _defaultStyles(self): """Set the default styles. """ - # Add Font - # ======== - - theAttr = {} - theAttr[_mkTag("style", "name")] = self.textFont - theAttr[_mkTag("style", "font-pitch")] = self._fontPitch - xStyl = etree.SubElement(self._xFont, _mkTag("style", "font-face"), attrib=theAttr) - # Add Paragraph Family Style # ========================== diff --git a/nw/gui/build.py b/nw/gui/build.py index 09d7d7e9..aff39c05 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -755,10 +755,9 @@ class GuiBuildNovel(QDialog): # Create the settings if theFormat == self.FMT_ODT: - byteFmt.append("odf") fileExt = "odt" textFmt = "Open Document" - outTool = "Qt" + outTool = "NW_ODT" elif theFormat == self.FMT_FODT: fileExt = "fodt" @@ -911,12 +910,20 @@ class GuiBuildNovel(QDialog): elif outTool == "NW_ODT": if theFormat == self.FMT_FODT: - makeOdt = ToOdt(self.theProject, self.theParent) + makeOdt = ToOdt(self.theProject, self.theParent, isFlat=True) self._doBuild(makeOdt) try: - with open(savePath, mode="wb") as outFile: - outFile.write(makeOdt.theResult) + makeOdt.saveFlatXML(savePath) + wSuccess = True + except Exception as e: + errMsg = str(e) + + elif theFormat == self.FMT_ODT: + makeOdt = ToOdt(self.theProject, self.theParent, isFlat=False) + self._doBuild(makeOdt) + try: + makeOdt.saveOpenDocText(savePath) wSuccess = True except Exception as e: diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py index ecf1af8d..6126be99 100644 --- a/tests/test_core/test_core_toodt.py +++ b/tests/test_core/test_core_toodt.py @@ -52,7 +52,7 @@ def testCoreToOdt_Convert(tmpConf, dummyGUI): theProject = NWProject(dummyGUI) dummyGUI.theIndex = NWIndex(theProject, dummyGUI) - theDoc = ToOdt(theProject, dummyGUI) + theDoc = ToOdt(theProject, dummyGUI, isFlat=True) # Export Mode # ===========