diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py index 562ae052..07ce3c81 100644 --- a/novelwriter/core/docbuild.py +++ b/novelwriter/core/docbuild.py @@ -334,14 +334,14 @@ class NWBuildDocument: bldObj.setStyles(self._build.getBool("html.addStyles")) bldObj.setReplaceUnicode(self._build.getBool("format.stripUnicode")) - if isinstance(bldObj, ToOdt): + if isinstance(bldObj, (ToOdt, ToDocX)): bldObj.setLanguage(self._project.data.language) bldObj.setHeaderFormat( self._build.getStr("odt.pageHeader"), self._build.getInt("odt.pageCountOffset"), ) - if isinstance(bldObj, (ToOdt, ToQTextDocument)): + if isinstance(bldObj, (ToOdt, ToDocX, ToQTextDocument)): scale = nwLabels.UNIT_SCALE.get(self._build.getStr("format.pageUnit"), 1.0) pW, pH = nwLabels.PAPER_SIZE.get(self._build.getStr("format.pageSize"), (-1.0, -1.0)) bldObj.setPageLayout( diff --git a/novelwriter/formats/todocx.py b/novelwriter/formats/todocx.py index 69d020e6..5040b8ab 100644 --- a/novelwriter/formats/todocx.py +++ b/novelwriter/formats/todocx.py @@ -32,6 +32,7 @@ from zipfile import ZipFile from novelwriter import __version__ from novelwriter.common import xmlIndent +from novelwriter.constants import nwStyles from novelwriter.core.project import NWProject from novelwriter.formats.tokenizer import Tokenizer @@ -41,24 +42,29 @@ logger = logging.getLogger(__name__) WORD_BASE = "application/vnd.openxmlformats-officedocument" RELS_TYPE = "application/vnd.openxmlformats-package.relationships+xml" REL_CORE = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" -REL_APP = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" -REL_DOC = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" +REL_BASE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" # Main XML NameSpaces PROPS_NS = "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" TYPES_NS = "http://schemas.openxmlformats.org/package/2006/content-types" RELS_NS = "http://schemas.openxmlformats.org/package/2006/relationships" +W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" XML_NS = { + "w": W_NS, "cp": "http://schemas.openxmlformats.org/package/2006/metadata/core-properties", "dc": "http://purl.org/dc/elements/1.1/", - "dcterms": "http://purl.org/dc/terms/", - "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main", "xsi": "http://www.w3.org/2001/XMLSchema-instance", + "dcterms": "http://purl.org/dc/terms/", } for ns, uri in XML_NS.items(): ET.register_namespace(ns, uri) +def _wTag(tag: str) -> str: + """Assemble namespace and tag name for standard w namespace.""" + return f"{{{W_NS}}}{tag}" + + def _mkTag(ns: str, tag: str) -> str: """Assemble namespace and tag name.""" if uri := XML_NS.get(ns, ""): @@ -90,12 +96,19 @@ class ToDocX(Tokenizer): super().__init__(project) # XML - self._dDoc = ET.Element("") # document.xml + self._dDoc = ET.Element("") # document.xml + self._dStyl = ET.Element("") # styles.xml self._xBody = ET.Element("") # Text body + # Properties + self._headerFormat = "" + self._pageOffset = 0 + # Internal - self._dLanguage = "en-GB" + self._fontFamily = "Liberation Serif" + self._fontSize = 12.0 + self._dLanguage = "en-GB" return @@ -106,18 +119,41 @@ class ToDocX(Tokenizer): def setLanguage(self, language: str | None) -> None: """Set language for the document.""" if language: - self._dLanguage = language.replace("_", "-") + self._dLanguage = language + return + + def setPageLayout( + self, width: float, height: float, top: float, bottom: float, left: float, right: float + ) -> None: + """Set the document page size and margins in millimetres.""" + return + + def setHeaderFormat(self, format: str, offset: int) -> None: + """Set the document header format.""" + self._headerFormat = format.strip() + self._pageOffset = offset return ## # Class Methods ## + def _emToSz(self, scale: float) -> int: + return int() + def initDocument(self) -> None: """Initialises the DocX document structure.""" - self._dDoc = ET.Element(_mkTag("w", "document")) - self._xBody = ET.SubElement(self._dDoc, _mkTag("w", "body")) + self._fontFamily = self._textFont.family() + self._fontSize = self._textFont.pointSizeF() + + self._dDoc = ET.Element(_wTag("document")) + self._dStyl = ET.Element(_wTag("styles")) + + self._xBody = ET.SubElement(self._dDoc, _wTag("body")) + + self._defaultStyles() + self._useableStyles() return @@ -136,10 +172,10 @@ class ToDocX(Tokenizer): "Id": "rId1", "Type": REL_CORE, "Target": "docProps/core.xml", }) _addSingle(dRels, "Relationship", attrib={ - "Id": "rId2", "Type": REL_APP, "Target": "docProps/app.xml", + "Id": "rId2", "Type": f"{REL_BASE}/extended-properties", "Target": "docProps/app.xml", }) _addSingle(dRels, "Relationship", attrib={ - "Id": "rId3", "Type": REL_DOC, "Target": "word/document.xml", + "Id": "rId3", "Type": f"{REL_BASE}/officeDocument", "Target": "word/document.xml", }) # core.xml @@ -169,6 +205,9 @@ class ToDocX(Tokenizer): # document.xml.rels dDRels = ET.Element("Relationships", attrib={"xmlns": RELS_NS}) + _addSingle(dDRels, "Relationship", attrib={ + "Id": "rId1", "Type": f"{REL_BASE}/styles", "Target": "styles.xml", + }) # [Content_Types].xml dCont = ET.Element("Types", attrib={"xmlns": TYPES_NS}) @@ -198,6 +237,10 @@ class ToDocX(Tokenizer): "PartName": "/word/document.xml", "ContentType": f"{WORD_BASE}.wordprocessingml.document.main+xml", }) + _addSingle(dCont, "Override", attrib={ + "PartName": "/word/styles.xml", + "ContentType": f"{WORD_BASE}.wordprocessingml.styles+xml", + }) def xmlToZip(name: str, xObj: ET.Element, zipObj: ZipFile) -> None: with zipObj.open(name, mode="w") as fObj: @@ -211,6 +254,137 @@ class ToDocX(Tokenizer): xmlToZip("docProps/app.xml", dApp, outZip) xmlToZip("word/_rels/document.xml.rels", dDRels, outZip) xmlToZip("word/document.xml", self._dDoc, outZip) + xmlToZip("word/styles.xml", self._dStyl, outZip) xmlToZip("[Content_Types].xml", dCont, outZip) return + + ## + # Internal Functions + ## + + def _defaultStyles(self) -> None: + """Set the default styles.""" + xStyl = ET.SubElement(self._dStyl, _wTag("docDefaults")) + xRDef = ET.SubElement(xStyl, _wTag("rPrDefault")) + xPDef = ET.SubElement(xStyl, _wTag("pPrDefault")) + xRPr = ET.SubElement(xRDef, _wTag("rPr")) + xPPr = ET.SubElement(xPDef, _wTag("pPr")) + + size = str(int(2.0 * self._fontSize)) + line = str(int(2.0 * self._lineHeight * self._fontSize)) + + ET.SubElement(xRPr, _wTag("rFonts"), attrib={ + _wTag("ascii"): self._fontFamily, + _wTag("hAnsi"): self._fontFamily, + _wTag("cs"): self._fontFamily, + }) + ET.SubElement(xRPr, _wTag("sz"), attrib={_wTag("val"): size}) + ET.SubElement(xRPr, _wTag("szCs"), attrib={_wTag("val"): size}) + ET.SubElement(xRPr, _wTag("lang"), attrib={_wTag("val"): self._dLanguage}) + ET.SubElement(xPPr, _wTag("spacing"), attrib={_wTag("line"): line}) + + return + + def _useableStyles(self) -> None: + """Set the usable styles.""" + # Add Normal Style + self._addParStyle( + name="Normal", + styleId="Normal", + size=1.0, + default=True, + margins=self._marginText, + ) + + # Add Heading 1 + hScale = self._scaleHeads + self._addParStyle( + name="Heading 1", + styleId="Heading1", + size=nwStyles.H_SIZES[1] if hScale else 1.0, + basedOn="Normal", + nextStyle="Normal", + margins=self._marginHead1, + level=0, + ) + + # Add Heading 2 + hScale = self._scaleHeads + self._addParStyle( + name="Heading 2", + styleId="Heading2", + size=nwStyles.H_SIZES[2] if hScale else 1.0, + basedOn="Normal", + nextStyle="Normal", + margins=self._marginHead2, + level=1, + ) + + # Add Heading 3 + hScale = self._scaleHeads + self._addParStyle( + name="Heading 3", + styleId="Heading3", + size=nwStyles.H_SIZES[3] if hScale else 1.0, + basedOn="Normal", + nextStyle="Normal", + margins=self._marginHead3, + level=1, + ) + + # Add Heading 4 + hScale = self._scaleHeads + self._addParStyle( + name="Heading 4", + styleId="Heading4", + size=nwStyles.H_SIZES[4] if hScale else 1.0, + basedOn="Normal", + nextStyle="Normal", + margins=self._marginHead4, + level=1, + ) + + return + + def _addParStyle( + self, *, + name: str, + styleId: str, + size: float, + basedOn: str | None = None, + nextStyle: str | None = None, + margins: tuple[float, float] | None = None, + default: bool = False, + level: int | None = None, + ) -> None: + """Add a paragraph style.""" + sAttr = {} + sAttr[_wTag("type")] = "paragraph" + sAttr[_wTag("styleId")] = styleId + if default: + sAttr[_wTag("default")] = "1" + + sz = str(int(2.0 * size * self._fontSize)) + + xStyl = ET.SubElement(self._dStyl, _wTag("style"), attrib=sAttr) + ET.SubElement(xStyl, _wTag("name"), attrib={_wTag("val"): name}) + if basedOn: + ET.SubElement(xStyl, _wTag("basedOn"), attrib={_wTag("val"): basedOn}) + if nextStyle: + ET.SubElement(xStyl, _wTag("next"), attrib={_wTag("val"): nextStyle}) + if level is not None: + ET.SubElement(xStyl, _wTag("outlineLvl"), attrib={_wTag("val"): str(level)}) + + xPPr = ET.SubElement(xStyl, _wTag("pPr")) + if margins: + ET.SubElement(xPPr, _wTag("spacing"), attrib={ + _wTag("before"): str(int(20.0 * margins[0])), + _wTag("after"): str(int(20.0 * margins[1])), + }) + + xRPr = ET.SubElement(xStyl, _wTag("rPr")) + ET.SubElement(xRPr, _wTag("sz"), attrib={_wTag("val"): sz}) + ET.SubElement(xRPr, _wTag("szCs"), attrib={_wTag("val"): sz}) + + return diff --git a/novelwriter/formats/toodt.py b/novelwriter/formats/toodt.py index ce993a25..5e68afdc 100644 --- a/novelwriter/formats/toodt.py +++ b/novelwriter/formats/toodt.py @@ -258,8 +258,7 @@ class ToOdt(Tokenizer): return def setPageLayout( - self, width: int | float, height: int | float, - top: int | float, bottom: int | float, left: int | float, right: int | float + self, width: float, height: float, top: float, bottom: float, left: float, right: float ) -> None: """Set the document page size and margins in millimetres.""" self._mDocWidth = f"{width/10.0:.3f}cm"