Files
novelWriter/nw/core/toodt.py
T
2021-01-28 00:21:39 +01:00

1194 lines
40 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
novelWriter ODT Text Converter
================================
Extends the Tokenizer class to generate ODT and FODT files
File History:
Created: 2021-01-26 [1.1rc1]
This file is a part of novelWriter
Copyright 20182021, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import nw
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
logger = logging.getLogger(__name__)
XML_NS = {
"office" : "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
"style" : "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
"loext" : "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
"text" : "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
"meta" : "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
"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"]
TAG_STNM = "{%s}style-name" % XML_NS["text"]
class ToOdt(Tokenizer):
X_BLD = 0x01
X_ITA = 0x02
X_DEL = 0x04
X_BRK = 0x08
X_TAB = 0x10
def __init__(self, theProject, theParent, isFlat):
Tokenizer.__init__(self, theProject, theParent)
self.mainConf = nw.CONFIG
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._mainPara = {}
self._autoPara = {}
self._autoText = {}
# Properties
self.textFont = "Liberation Serif"
self.textSize = 12
self.textFixed = False
self.colourHead = False
# Internal
self._fontFamily = "&apos;Liberation Sans&apos;"
self._fontPitch = "variable"
self._fSizeTitle = "30pt"
self._fSizeHead1 = "24pt"
self._fSizeHead2 = "20pt"
self._fSizeHead3 = "16pt"
self._fSizeHead4 = "14pt"
self._fSizeHead = "14pt"
self._fSizeText = "12pt"
self._lineHeight = "115%"
self._textAlign = "left"
self._dLanguage = "en"
self._dCountry = "GB"
## Text Margings in Units of em
self._mTopTitle = "0.423cm"
self._mTopHead1 = "0.423cm"
self._mTopHead2 = "0.353cm"
self._mTopHead3 = "0.247cm"
self._mTopHead4 = "0.247cm"
self._mTopHead = "0.423cm"
self._mTopText = "0.000cm"
self._mBotTitle = "0.212cm"
self._mBotHead1 = "0.212cm"
self._mBotHead2 = "0.212cm"
self._mBotHead3 = "0.212cm"
self._mBotHead4 = "0.212cm"
self._mBotHead = "0.212cm"
self._mBotText = "0.247cm"
## Colour
self._colHead12 = None
self._opaHead12 = None
self._colHead34 = None
self._opaHead34 = None
self._colMetaTx = None
self._opaMetaTx = None
return
##
# Setters
##
def setLanguage(self, theLang):
"""Set language for the document.
"""
if theLang is None:
return False
langBits = theLang.split("_")
self._dLanguage = langBits[0]
if len(langBits) > 1:
self._dCountry = langBits[1]
return True
def setColourHeaders(self, doColour):
self.colourHead = doColour
return
##
# Class Methods
##
def initDocument(self):
"""Initialises a new open document XML tree.
"""
# Initialise Variables
# ====================
self._fontFamily = self.textFont
if len(self.textFont.split()) > 1:
self._fontFamily = f"&apos;{self.textFont}&apos;"
self._fontPitch = "fixed" if self.textFixed else "variable"
self._fSizeTitle = f"{round(2.50 * self.textSize):d}pt"
self._fSizeHead1 = f"{round(2.00 * self.textSize):d}pt"
self._fSizeHead2 = f"{round(1.60 * self.textSize):d}pt"
self._fSizeHead3 = f"{round(1.30 * self.textSize):d}pt"
self._fSizeHead4 = f"{round(1.15 * self.textSize):d}pt"
self._fSizeHead = f"{round(1.15 * self.textSize):d}pt"
self._fSizeText = f"{self.textSize:d}pt"
self._mTopTitle = self._emToCm(self.marginTitle[0])
self._mTopHead1 = self._emToCm(self.marginHead1[0])
self._mTopHead2 = self._emToCm(self.marginHead2[0])
self._mTopHead3 = self._emToCm(self.marginHead3[0])
self._mTopHead4 = self._emToCm(self.marginHead4[0])
self._mTopHead = self._emToCm(self.marginHead4[0])
self._mTopText = self._emToCm(self.marginText[0])
self._mBotTitle = self._emToCm(self.marginTitle[1])
self._mBotHead1 = self._emToCm(self.marginHead1[1])
self._mBotHead2 = self._emToCm(self.marginHead2[1])
self._mBotHead3 = self._emToCm(self.marginHead3[1])
self._mBotHead4 = self._emToCm(self.marginHead4[1])
self._mBotHead = self._emToCm(self.marginHead4[1])
self._mBotText = self._emToCm(self.marginText[1])
if self.colourHead:
self._colHead12 = "#2a6099"
self._opaHead12 = "100%"
self._colHead34 = "#444444"
self._opaHead34 = "100%"
self._colMetaTx = "#666666"
self._opaMetaTx = "100%"
self._lineHeight = f"{round(100 * self.lineHeight):d}%"
self._textAlign = "justify" if self.doJustify else "left"
# 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()
return
def doConvert(self):
"""Convert the list of text tokens into a HTML document saved
to theResult.
"""
self.theResult = ""
odtTags = {
self.FMT_B_B : "_B",
self.FMT_B_E : "b_",
self.FMT_I_B : "I",
self.FMT_I_E : "i",
self.FMT_D_B : "_S",
self.FMT_D_E : "s_",
}
thisPar = []
thisFmt = []
parStyle = None
hasHardBreak = False
for tType, tLine, tText, tFormat, tStyle in self.theTokens:
# Styles
oStyle = ODTParagraphStyle()
if tStyle is not None:
if tStyle & self.A_LEFT:
oStyle.setTextAlign("left")
if tStyle & self.A_RIGHT:
oStyle.setTextAlign("right")
if tStyle & self.A_CENTRE:
oStyle.setTextAlign("center")
if tStyle & self.A_JUSTIFY:
oStyle.setTextAlign("justify")
if tStyle & self.A_PBB:
oStyle.setBreakBefore("page")
if tStyle & self.A_PBB_AUT:
oStyle.setBreakBefore("auto")
if tStyle & self.A_PBA:
oStyle.setBreakAfter("page")
if tStyle & self.A_PBA_AUT:
oStyle.setBreakAfter("auto")
# Process Text Type
if tType == self.T_EMPTY:
if hasHardBreak and parStyle is not None:
if self.doJustify:
parStyle.setTextAlign("left")
if len(thisPar) > 0:
tTemp = "".join(thisPar)
fTemp = "".join(thisFmt)
tTxt = tTemp.rstrip()
tFmt = fTemp[:len(tTxt)]
self._addTextPar("Text_Body", parStyle, tTxt, theFmt=tFmt)
thisPar = []
thisFmt = []
parStyle = None
hasHardBreak = False
elif tType == self.T_TITLE:
tHead = tText.replace(r"\\", "\n")
self._addTextPar("Title", oStyle, tHead, isHead=True)
elif tType == self.T_HEAD1:
tHead = tText.replace(r"\\", "\n")
self._addTextPar("Heading_1", oStyle, tHead, isHead=True, oLevel="1")
elif tType == self.T_HEAD2:
tHead = tText.replace(r"\\", "\n")
self._addTextPar("Heading_2", oStyle, tHead, isHead=True, oLevel="2")
elif tType == self.T_HEAD3:
tHead = tText.replace(r"\\", "\n")
self._addTextPar("Heading_3", oStyle, tHead, isHead=True, oLevel="3")
elif tType == self.T_HEAD4:
tHead = tText.replace(r"\\", "\n")
self._addTextPar("Heading_4", oStyle, tHead, isHead=True, oLevel="4")
elif tType == self.T_SEP:
self._addTextPar("Text_Body", oStyle, tText)
elif tType == self.T_SKIP:
self._addTextPar("Text_Body", oStyle, "")
elif tType == self.T_TEXT:
tTemp = tText
if parStyle is None:
parStyle = oStyle
tFmt = " "*len(tTemp)
for xPos, xLen, xFmt in tFormat:
tFmt = tFmt[:xPos] + odtTags[xFmt] + tFmt[xPos+xLen:]
tTxt = tTemp.rstrip()
tFmt = tFmt[:len(tTxt)]
if tText.endswith(" "):
thisPar.append(tTxt + "\n")
thisFmt.append(tFmt + " ")
hasHardBreak = True
else:
thisPar.append(tTxt + " ")
thisFmt.append(tFmt + " ")
elif tType == self.T_SYNOPSIS and self.doSynopsis:
tTemp, fTemp = self._formatSynopsis(tText)
self._addTextPar("Text_Meta", oStyle, tTemp, theFmt=fTemp)
elif tType == self.T_COMMENT and self.doComments:
tTemp, fTemp = self._formatComments(tText)
self._addTextPar("Text_Meta", oStyle, tTemp, theFmt=fTemp)
elif tType == self.T_KEYWORD and self.doKeywords:
tTemp, fTemp = self._formatKeywords(tText)
self._addTextPar("Text_Meta", oStyle, tTemp, theFmt=fTemp)
return
def closeDocument(self):
"""Return the serialised XML document
"""
# Build the auto-generated styles
for styleName, styleObj in self._autoPara.values():
styleObj.packXML(self._xAuto, styleName)
for styleName, styleObj in self._autoText.values():
styleObj.packXML(self._xAuto, styleName)
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
##
# Internal Functions
##
def _formatSynopsis(self, tText):
"""Apply formatting to synopsis lines.
"""
rTxt = "**Synopsis:** %s" % tText
rFmt = "_B b_ %s" % (" "*len(tText))
return rTxt, rFmt
def _formatComments(self, tText):
"""Apply formatting to comments.
"""
rTxt = "**Comment:** %s" % tText
rFmt = "_B b_ %s" % (" "*len(tText))
return rTxt, rFmt
def _formatKeywords(self, tText):
"""Apply formatting to keywords.
"""
isValid, theBits, thePos = self.theParent.theIndex.scanThis("@"+tText)
if not isValid or not theBits:
return ""
rTxt = ""
rFmt = ""
if theBits[0] in nwLabels.KEY_NAME:
tText = nwLabels.KEY_NAME[theBits[0]]
rTxt += "**%s:** " % tText
rFmt += "_B%s b_ " % (" "*len(tText))
if len(theBits) > 1:
if theBits[0] == nwKeyWords.TAG_KEY:
rTxt += "%s" % theBits[1]
rFmt += "%s" % (" "*len(theBits[1]))
else:
tTags = ", ".join(theBits[1:])
rTxt += tTags
rFmt += (" "*len(tTags))
return rTxt, rFmt
def _addTextPar(self, styleName, oStyle, theText, theFmt="", isHead=False, oLevel=None):
"""Add a text paragraph to the text XML element.
"""
tAttr = {}
tAttr[_mkTag("text", "style-name")] = self._paraStyle(styleName, oStyle)
if oLevel is not None:
tAttr[_mkTag("text", "outline-level")] = oLevel
pTag = "h" if isHead else "p"
xElem = etree.SubElement(self._xText, _mkTag("text", pTag), attrib=tAttr)
if not theText:
return
##
# Process Formatting
##
if len(theText) != len(theFmt):
# Genrate dummy format if there isn't any
theFmt = " "*len(theText)
# XML functions
xTail = None
def appendText(tText):
nonlocal xElem, xTail
if tText:
if xTail is None:
xElem.text = tText
else:
xTail.tail = tText
def appendSpan(tText, tFmt):
nonlocal xElem, xTail
if tText:
xTail = etree.SubElement(xElem, TAG_SPAN, attrib={
TAG_STNM: self._textStyle(tFmt)
})
xTail.text = tText
# The formatting loop
tTemp = ""
xFmt = 0x00
pFmt = 0x00
for i, c in enumerate(theText):
if theFmt[i] == "_":
continue
elif theFmt[i] == "B":
xFmt |= self.X_BLD
elif theFmt[i] == "b":
xFmt ^= self.X_BLD
elif theFmt[i] == "I":
xFmt |= self.X_ITA
elif theFmt[i] == "i":
xFmt ^= self.X_ITA
elif theFmt[i] == "S":
xFmt |= self.X_DEL
elif theFmt[i] == "s":
xFmt ^= self.X_DEL
if c == "\n":
xFmt |= self.X_BRK
c = ""
elif c == "\t":
xFmt |= self.X_TAB
c = ""
if theFmt[i] == " ":
tTemp += c
if xFmt != pFmt:
if pFmt == 0x00:
appendText(tTemp)
tTemp = ""
else:
appendSpan(tTemp, pFmt)
tTemp = ""
if xFmt & self.X_BRK:
xTail = etree.SubElement(xElem, TAG_BR)
xFmt ^= self.X_BRK
if xFmt & self.X_TAB:
xTail = etree.SubElement(xElem, TAG_TAB)
xFmt ^= self.X_TAB
pFmt = xFmt
# Save what remains in the buffer
appendText(tTemp)
return
def _paraStyle(self, parName, oStyle):
"""Return a name for a style object.
"""
refStyle = self._mainPara.get(parName, None)
if refStyle is None:
logger.error("Unknown paragraph style '%s'" % parName)
return "Standard"
if not refStyle.checkNew(oStyle):
return parName
oStyle.setParentStyleName(parName)
theID = oStyle.getID()
if theID in self._autoPara:
return self._autoPara[theID][0]
newName = "P%d" % (len(self._autoPara) + 1)
self._autoPara[theID] = (newName, oStyle)
return newName
def _textStyle(self, styleCode):
"""Return a text style for a given style code.
"""
if styleCode in self._autoText:
return self._autoText[styleCode][0]
newName = "T%d" % (len(self._autoText) + 1)
newStyle = ODTTextStyle()
if styleCode & self.X_BLD:
newStyle.setFontWeight("bold")
if styleCode & self.X_ITA:
newStyle.setFontStyle("italic")
if styleCode & self.X_DEL:
newStyle.setStrikeStyle("solid")
newStyle.setStrikeType("single")
self._autoText[styleCode] = (newName, newStyle)
return newName
def _emToCm(self, emVal):
"""Converts an em value to centimetres.
"""
return f"{emVal*2.54/72*self.textSize:.3f}cm"
##
# Style Elements
##
def _defaultStyles(self):
"""Set the default styles.
"""
# Add Paragraph Family Style
# ==========================
theAttr = {}
theAttr[_mkTag("style", "family")] = "paragraph"
xStyl = etree.SubElement(self._xStyl, _mkTag("style", "default-style"), attrib=theAttr)
theAttr = {}
theAttr[_mkTag("style", "line-break")] = "strict"
theAttr[_mkTag("style", "tab-stop-distance")] = "1.251cm"
theAttr[_mkTag("style", "writing-mode")] = "page"
etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
theAttr = {}
theAttr[_mkTag("style", "font-name")] = self.textFont
theAttr[_mkTag("fo", "font-family")] = self._fontFamily
theAttr[_mkTag("fo", "font-size")] = self._fSizeText
theAttr[_mkTag("fo", "language")] = self._dLanguage
theAttr[_mkTag("fo", "country")] = self._dCountry
etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
# Add Standard Paragraph Style
# ============================
theAttr = {}
theAttr[_mkTag("style", "name")] = "Standard"
theAttr[_mkTag("style", "family")] = "paragraph"
theAttr[_mkTag("style", "class")] = "text"
xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
theAttr = {}
theAttr[_mkTag("style", "font-name")] = self.textFont
theAttr[_mkTag("fo", "font-family")] = self._fontFamily
theAttr[_mkTag("fo", "font-size")] = self._fSizeText
etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
# Add Default Heading Style
# =========================
theAttr = {}
theAttr[_mkTag("style", "name")] = "Heading"
theAttr[_mkTag("style", "family")] = "paragraph"
theAttr[_mkTag("style", "parent-style-name")] = "Standard"
theAttr[_mkTag("style", "next-style-name")] = "Text_Body"
theAttr[_mkTag("style", "class")] = "text"
xStyl = etree.SubElement(self._xStyl, _mkTag("style", "style"), attrib=theAttr)
theAttr = {}
theAttr[_mkTag("fo", "margin-top")] = self._mTopHead
theAttr[_mkTag("fo", "margin-bottom")] = self._mBotHead
theAttr[_mkTag("fo", "keep-with-next")] = "always"
etree.SubElement(xStyl, _mkTag("style", "paragraph-properties"), attrib=theAttr)
theAttr = {}
theAttr[_mkTag("style", "font-name")] = self.textFont
theAttr[_mkTag("fo", "font-family")] = self._fontFamily
theAttr[_mkTag("fo", "font-size")] = self._fSizeHead
etree.SubElement(xStyl, _mkTag("style", "text-properties"), attrib=theAttr)
return
def _useableStyles(self):
"""Set the usable styles.
"""
# Add Text Body Style
# ===================
oStyle = ODTParagraphStyle()
oStyle.setDisplayName("Text Body")
oStyle.setParentStyleName("Standard")
oStyle.setClass("text")
oStyle.setMarginTop(self._mTopText)
oStyle.setMarginBottom(self._mBotText)
oStyle.setLineHeight(self._lineHeight)
oStyle.setFontName(self.textFont)
oStyle.setFontFamily(self._fontFamily)
oStyle.setFontSize(self._fSizeText)
oStyle.setTextAlign(self._textAlign)
oStyle.packXML(self._xStyl, "Text_Body")
self._mainPara["Text_Body"] = oStyle
# Add Text Meta Style
# ===================
oStyle = ODTParagraphStyle()
oStyle.setDisplayName("Text Meta")
oStyle.setParentStyleName("Standard")
oStyle.setClass("text")
oStyle.setMarginTop(self._mTopText)
oStyle.setMarginBottom(self._mBotText)
oStyle.setLineHeight(self._lineHeight)
oStyle.setFontName(self.textFont)
oStyle.setFontFamily(self._fontFamily)
oStyle.setFontSize(self._fSizeText)
oStyle.setColor(self._colMetaTx)
oStyle.setOpacity(self._opaMetaTx)
oStyle.packXML(self._xStyl, "Text_Meta")
self._mainPara["Text_Meta"] = oStyle
# Add Title Style
# ===============
oStyle = ODTParagraphStyle()
oStyle.setDisplayName("Title")
oStyle.setParentStyleName("Heading")
oStyle.setNextStyleName("Text_Body")
oStyle.setClass("chapter")
oStyle.setTextAlign("center")
oStyle.setMarginTop(self._mTopTitle)
oStyle.setMarginBottom(self._mBotTitle)
oStyle.setFontName(self.textFont)
oStyle.setFontFamily(self._fontFamily)
oStyle.setFontSize(self._fSizeTitle)
oStyle.setFontWeight("bold")
oStyle.packXML(self._xStyl, "Title")
self._mainPara["Title"] = oStyle
# Add Heading 1 Style
# ===================
oStyle = ODTParagraphStyle()
oStyle.setDisplayName("Heading 1")
oStyle.setParentStyleName("Heading")
oStyle.setNextStyleName("Text_Body")
oStyle.setOutlineLevel("1")
oStyle.setClass("text")
oStyle.setMarginTop(self._mTopHead1)
oStyle.setMarginBottom(self._mBotHead1)
oStyle.setFontName(self.textFont)
oStyle.setFontFamily(self._fontFamily)
oStyle.setFontSize(self._fSizeHead1)
oStyle.setColor(self._colHead12)
oStyle.setOpacity(self._opaHead12)
oStyle.setFontWeight("bold")
oStyle.packXML(self._xStyl, "Heading_1")
self._mainPara["Heading_1"] = oStyle
# Add Heading 2 Style
# ===================
oStyle = ODTParagraphStyle()
oStyle.setDisplayName("Heading 2")
oStyle.setParentStyleName("Heading")
oStyle.setNextStyleName("Text_Body")
oStyle.setOutlineLevel("2")
oStyle.setClass("text")
oStyle.setMarginTop(self._mTopHead2)
oStyle.setMarginBottom(self._mBotHead2)
oStyle.setFontName(self.textFont)
oStyle.setFontFamily(self._fontFamily)
oStyle.setFontSize(self._fSizeHead2)
oStyle.setColor(self._colHead12)
oStyle.setOpacity(self._opaHead12)
oStyle.setFontWeight("bold")
oStyle.packXML(self._xStyl, "Heading_2")
self._mainPara["Heading_2"] = oStyle
# Add Heading 3 Style
# ===================
oStyle = ODTParagraphStyle()
oStyle.setDisplayName("Heading 3")
oStyle.setParentStyleName("Heading")
oStyle.setNextStyleName("Text_Body")
oStyle.setOutlineLevel("3")
oStyle.setClass("text")
oStyle.setMarginTop(self._mTopHead3)
oStyle.setMarginBottom(self._mBotHead3)
oStyle.setFontName(self.textFont)
oStyle.setFontFamily(self._fontFamily)
oStyle.setFontSize(self._fSizeHead3)
oStyle.setColor(self._colHead34)
oStyle.setOpacity(self._opaHead34)
oStyle.setFontWeight("bold")
oStyle.packXML(self._xStyl, "Heading_3")
self._mainPara["Heading_3"] = oStyle
# Add Heading 4 Style
# ===================
oStyle = ODTParagraphStyle()
oStyle.setDisplayName("Heading 4")
oStyle.setParentStyleName("Heading")
oStyle.setNextStyleName("Text_Body")
oStyle.setOutlineLevel("4")
oStyle.setClass("text")
oStyle.setMarginTop(self._mTopHead4)
oStyle.setMarginBottom(self._mBotHead4)
oStyle.setFontName(self.textFont)
oStyle.setFontFamily(self._fontFamily)
oStyle.setFontSize(self._fSizeHead4)
oStyle.setColor(self._colHead34)
oStyle.setOpacity(self._opaHead34)
oStyle.setFontWeight("bold")
oStyle.packXML(self._xStyl, "Heading_4")
self._mainPara["Heading_4"] = oStyle
return
# END Class ToOdt
# =============================================================================================== #
# Auto-Style Classes
# =============================================================================================== #
class ODTParagraphStyle():
"""Wrapper class for the paragraph style setting used by the
exporter. Only the used settings are exposed here to keep the class
minimal and fast.
"""
VALID_ALIGN = ["start", "center", "end", "justify", "inside", "outside", "left", "right"]
VALID_BREAK = ["auto", "column", "page", "even-page", "odd-page", "inherit"]
VALID_LEVEL = ["1", "2", "3", "4"]
VALID_CLASS = ["text", "chapter"]
VALID_WEIGHT = ["normal", "inherit", "bold"]
def __init__(self):
# Attributes
self._mAttr = {
"display-name": ["style", None],
"parent-style-name": ["style", None],
"next-style-name": ["style", None],
"default-outline-level": ["style", None],
"class": ["style", None],
}
# Paragraph Attributes
self._pAttr = {
"margin-top": ["fo", None],
"margin-bottom": ["fo", None],
"line-height": ["fo", None],
"text-align": ["fo", None],
"break-before": ["fo", None],
"break-after": ["fo", None],
}
# Text Attributes
self._tAttr = {
"font-name": ["style", None],
"font-family": ["fo", None],
"font-size": ["fo", None],
"font-weight": ["fo", None],
"color": ["fo", None],
"opacity": ["loext", None],
}
return
##
# Attribute Setters
##
def setDisplayName(self, theValue):
self._mAttr["display-name"][1] = str(theValue)
return
def setParentStyleName(self, theValue):
self._mAttr["parent-style-name"][1] = str(theValue)
return
def setNextStyleName(self, theValue):
self._mAttr["next-style-name"][1] = str(theValue)
return
def setOutlineLevel(self, theValue):
if theValue in self.VALID_LEVEL:
self._mAttr["default-outline-level"][1] = str(theValue)
return
def setClass(self, theValue):
if theValue in self.VALID_CLASS:
self._mAttr["class"][1] = str(theValue)
return
##
# Paragraph Setters
##
def setMarginTop(self, theValue):
self._pAttr["margin-top"][1] = str(theValue)
return
def setMarginBottom(self, theValue):
self._pAttr["margin-bottom"][1] = str(theValue)
return
def setLineHeight(self, theValue):
self._pAttr["line-height"][1] = str(theValue)
return
def setTextAlign(self, theValue):
if theValue in self.VALID_ALIGN:
self._pAttr["text-align"][1] = str(theValue)
return
def setBreakBefore(self, theValue):
if theValue in self.VALID_BREAK:
self._pAttr["break-before"][1] = str(theValue)
return
def setBreakAfter(self, theValue):
if theValue in self.VALID_BREAK:
self._pAttr["break-after"][1] = str(theValue)
return
##
# Text Setters
##
def setFontName(self, theValue):
self._tAttr["font-name"][1] = str(theValue)
return
def setFontFamily(self, theValue):
self._tAttr["font-family"][1] = str(theValue)
return
def setFontSize(self, theValue):
self._tAttr["font-size"][1] = str(theValue)
return
def setFontWeight(self, theValue):
if theValue in self.VALID_WEIGHT:
self._tAttr["font-weight"][1] = str(theValue)
return
def setColor(self, theValue):
self._tAttr["color"][1] = str(theValue)
return
def setOpacity(self, theValue):
self._tAttr["opacity"][1] = str(theValue)
return
##
# Getters
##
def getAttr(self, attrName):
"""Look through the dictionaries for the value, and return it if
we can find it, If not, return None.
"""
retVal = self._mAttr.get(attrName, None)
if retVal is not None:
return retVal
retVal = self._pAttr.get(attrName, None)
if retVal is not None:
return retVal
retVal = self._tAttr.get(attrName, None)
if retVal is not None:
return retVal
return None
##
# Methods
##
def checkNew(self, refStyle):
"""Check if there are new settings in refStyle that differ from
those in the current object.
"""
for aName, (aNm, aVal) in refStyle._mAttr.items():
if aVal is not None and aVal != self._mAttr[aName][1]:
return True
for aName, (aNm, aVal) in refStyle._pAttr.items():
if aVal is not None and aVal != self._pAttr[aName][1]:
return True
for aName, (aNm, aVal) in refStyle._tAttr.items():
if aVal is not None and aVal != self._tAttr[aName][1]:
return True
return False
def getID(self):
"""Generate a unique ID from the settings.
"""
theString = (
f"Paragraph:Main:{str(self._mAttr)}:"
f"Paragraph:Para:{str(self._pAttr)}:"
f"Paragraph:Text:{str(self._tAttr)}:"
)
return sha256(theString.encode()).hexdigest()
def packXML(self, xParent, xName):
"""Pack the content into an xml element.
"""
theAttr = {}
theAttr[_mkTag("style", "name")] = xName
theAttr[_mkTag("style", "family")] = "paragraph"
for aName, (aNm, aVal) in self._mAttr.items():
if aVal is not None:
theAttr[_mkTag(aNm, aName)] = aVal
xEntry = etree.SubElement(xParent, _mkTag("style", "style"), attrib=theAttr)
theAttr = {}
for aName, (aNm, aVal) in self._pAttr.items():
if aVal is not None:
theAttr[_mkTag(aNm, aName)] = aVal
if theAttr:
etree.SubElement(xEntry, _mkTag("style", "paragraph-properties"), attrib=theAttr)
theAttr = {}
for aName, (aNm, aVal) in self._tAttr.items():
if aVal is not None:
theAttr[_mkTag(aNm, aName)] = aVal
if theAttr:
etree.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=theAttr)
return
# END Class ODTParagraphStyle
class ODTTextStyle():
"""Wrapper class for the text style setting used by the exporter.
Only the used settings are exposed here to keep the class minimal
and fast.
"""
VALID_WEIGHT = ["normal", "inherit", "bold"]
VALID_STYLE = ["normal", "inherit", "italic"]
VALID_LSTYLE = ["none", "solid"]
VALID_LTYPE = ["none", "single", "double"]
def __init__(self):
# Text Attributes
self._tAttr = {
"font-weight": ["fo", None],
"font-style": ["fo", None],
"text-line-through-style": ["style", None],
"text-line-through-type": ["style", None],
}
return
##
# Setters
##
def setFontWeight(self, theValue):
if theValue in self.VALID_WEIGHT:
self._tAttr["font-weight"][1] = str(theValue)
return
def setFontStyle(self, theValue):
if theValue in self.VALID_STYLE:
self._tAttr["font-style"][1] = str(theValue)
return
def setStrikeStyle(self, theValue):
if theValue in self.VALID_LSTYLE:
self._tAttr["text-line-through-style"][1] = str(theValue)
return
def setStrikeType(self, theValue):
if theValue in self.VALID_LTYPE:
self._tAttr["text-line-through-type"][1] = str(theValue)
return
##
# Methods
##
def packXML(self, xParent, xName):
"""Pack the content into an xml element.
"""
theAttr = {}
theAttr[_mkTag("style", "name")] = xName
theAttr[_mkTag("style", "family")] = "text"
xEntry = etree.SubElement(xParent, _mkTag("style", "style"), attrib=theAttr)
theAttr = {}
for aName, (aNm, aVal) in self._tAttr.items():
if aVal is not None:
theAttr[_mkTag(aNm, aName)] = aVal
if theAttr:
etree.SubElement(xEntry, _mkTag("style", "text-properties"), attrib=theAttr)
return
# END Class ODTTextStyle
# =============================================================================================== #
# Local Functions
# =============================================================================================== #
def _mkTag(nsName, tagName):
"""Assemble namespace and tag name.
"""
theNS = XML_NS.get(nsName, "")
if theNS:
return "{%s}%s" % (theNS, tagName)
logger.warning("Missing xml namespace '%s'" % nsName)
return tagName