Files
novelWriter/novelwriter/formats/toqdoc.py
T

494 lines
18 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.
"""
novelWriter QTextDocument Converter
=====================================
File History:
Created: 2024-05-21 [2.5b1] ToQTextDocument
This file is a part of novelWriter
Copyright (C) 2024 Veronica Berglyd Olsen and novelWriter contributors
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/>.
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from PyQt6.QtCore import QMarginsF, QSizeF
from PyQt6.QtGui import (
QColor, QFont, QFontDatabase, QPageLayout, QPageSize, QTextBlockFormat,
QTextCharFormat, QTextCursor, QTextDocument, QTextFrameFormat
)
from PyQt6.QtPrintSupport import QPrinter
from novelwriter import __version__
from novelwriter.constants import nwStyles, nwUnicode
from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt, stripEscape
from novelwriter.formats.tokenizer import HEADINGS, Tokenizer
from novelwriter.types import (
QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight,
QtKeepAnchor, QtMoveAnchor, QtPageBreakAfter, QtPageBreakAuto,
QtPageBreakBefore, QtPropLineHeight, QtTransparent, QtVAlignNormal,
QtVAlignSub, QtVAlignSuper
)
if TYPE_CHECKING:
from pathlib import Path
from novelwriter.core.project import NWProject
logger = logging.getLogger(__name__)
T_TextStyle = tuple[QTextBlockFormat, QTextCharFormat]
def newBlock(cursor: QTextCursor, bFmt: QTextBlockFormat) -> None:
if cursor.position() > 0:
cursor.insertBlock(bFmt)
else:
cursor.setBlockFormat(bFmt)
class ToQTextDocument(Tokenizer):
"""Core: QTextDocument Writer
Extend the Tokenizer class to generate a QTextDocument output. This
is intended for usage in the document viewer and build tool preview.
"""
def __init__(self, project: NWProject) -> None:
super().__init__(project)
self._document = QTextDocument()
self._document.setUndoRedoEnabled(False)
self._document.setDocumentMargin(0.0)
self._usedNotes: dict[str, int] = {}
self._usedFields: list[tuple[int, str]] = []
self._init = False
self._newPage = False
self._anchors = True
self._hWeight = QFont.Weight.Bold
self._dWeight = QFont.Weight.Normal
self._dItalic = False
self._dStrike = False
self._dUnderline = False
self._dpi = 96
self._pageSize = QPageSize(QPageSize.PageSizeId.A4)
self._pageMargins = QMarginsF(20.0, 20.0, 20.0, 20.0)
return
##
# Properties
##
@property
def document(self) -> QTextDocument:
"""Return the document."""
return self._document
##
# Setters
##
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."""
self._pageSize = QPageSize(QSizeF(width, height), QPageSize.Unit.Millimeter)
self._pageMargins = QMarginsF(left, top, right, bottom)
return
def setShowNewPage(self, state: bool) -> None:
"""Add markers for page breaks."""
self._newPage = state
return
def disableAnchors(self) -> None:
"""Disable anchors for when writing to file."""
self._anchors = False
return
##
# Class Methods
##
def initDocument(self, pdf: bool = False) -> None:
"""Initialise all computed values of the document."""
super().initDocument()
if pdf:
family = self._textFont.family()
style = self._textFont.styleName()
self._dpi = 1200 if QFontDatabase.isScalable(family, style) else 72
self._document.setUndoRedoEnabled(False)
self._document.blockSignals(True)
self._document.clear()
self._document.setDefaultFont(self._textFont)
# Default Styles
self._dWeight = self._textFont.weight()
self._dItalic = self._textFont.italic()
self._dStrike = self._textFont.strikeOut()
self._dUnderline = self._textFont.underline()
# Header Weight
self._hWeight = QFont.Weight.Bold if self._boldHeads else self._dWeight
# Scaled Sizes
# ============
fPt = self._textFont.pointSizeF()
fPx = fPt*96.0/72.0 # 1 em in pixels
mPx = fPx * self._dpi/96.0
self._mHead = {
BlockTyp.TITLE: (fPx * self._marginTitle[0], fPx * self._marginTitle[1]),
BlockTyp.PART: (fPx * self._marginTitle[0], fPx * self._marginTitle[1]),
BlockTyp.HEAD1: (fPx * self._marginHead1[0], fPx * self._marginHead1[1]),
BlockTyp.HEAD2: (fPx * self._marginHead2[0], fPx * self._marginHead2[1]),
BlockTyp.HEAD3: (fPx * self._marginHead3[0], fPx * self._marginHead3[1]),
BlockTyp.HEAD4: (fPx * self._marginHead4[0], fPx * self._marginHead4[1]),
}
hScale = self._scaleHeads
self._sHead = {
BlockTyp.TITLE: (nwStyles.H_SIZES.get(0, 1.0) * fPt) if hScale else fPt,
BlockTyp.PART: (nwStyles.H_SIZES.get(0, 1.0) * fPt) if hScale else fPt,
BlockTyp.HEAD1: (nwStyles.H_SIZES.get(1, 1.0) * fPt) if hScale else fPt,
BlockTyp.HEAD2: (nwStyles.H_SIZES.get(2, 1.0) * fPt) if hScale else fPt,
BlockTyp.HEAD3: (nwStyles.H_SIZES.get(3, 1.0) * fPt) if hScale else fPt,
BlockTyp.HEAD4: (nwStyles.H_SIZES.get(4, 1.0) * fPt) if hScale else fPt,
}
self._mText = (fPx * self._marginText[0], fPx * self._marginText[1])
self._mMeta = (fPx * self._marginMeta[0], fPx * self._marginMeta[1])
self._mSep = (fPx * self._marginSep[0], fPx * self._marginSep[1])
self._mIndent = mPx * 2.0
self._tIndent = mPx * self._firstWidth
# Text Formats
# ============
self._blockFmt = QTextBlockFormat()
self._blockFmt.setTopMargin(self._mText[0])
self._blockFmt.setBottomMargin(self._mText[1])
self._blockFmt.setAlignment(QtAlignAbsolute)
self._blockFmt.setLineHeight(100.0*self._lineHeight, QtPropLineHeight)
self._charFmt = QTextCharFormat()
self._charFmt.setBackground(QtTransparent)
self._charFmt.setForeground(self._theme.text)
self._init = True
return
def doConvert(self) -> None:
"""Write text tokens into the document."""
if not self._init:
return
self._document.blockSignals(True)
cursor = QTextCursor(self._document)
cursor.movePosition(QTextCursor.MoveOperation.End)
for tType, tMeta, tText, tFormat, tStyle in self._blocks:
bFmt = QTextBlockFormat(self._blockFmt)
if tType in (BlockTyp.COMMENT, BlockTyp.KEYWORD):
bFmt.setTopMargin(self._mMeta[0])
bFmt.setBottomMargin(self._mMeta[1])
elif tType == BlockTyp.SEP:
bFmt.setTopMargin(self._mSep[0])
bFmt.setBottomMargin(self._mSep[1])
if tStyle & BlockFmt.LEFT:
bFmt.setAlignment(QtAlignLeft)
elif tStyle & BlockFmt.RIGHT:
bFmt.setAlignment(QtAlignRight)
elif tStyle & BlockFmt.CENTRE:
bFmt.setAlignment(QtAlignCenter)
elif tStyle & BlockFmt.JUSTIFY:
bFmt.setAlignment(QtAlignJustify)
if tStyle & BlockFmt.PBB:
self._insertNewPageMarker(cursor)
bFmt.setPageBreakPolicy(QtPageBreakBefore)
if tStyle & BlockFmt.PBA:
bFmt.setPageBreakPolicy(QtPageBreakAfter)
if tStyle & BlockFmt.Z_BTM:
bFmt.setBottomMargin(0.0)
if tStyle & BlockFmt.Z_TOP:
bFmt.setTopMargin(0.0)
if tStyle & BlockFmt.IND_L:
bFmt.setLeftMargin(self._mIndent)
if tStyle & BlockFmt.IND_R:
bFmt.setRightMargin(self._mIndent)
if tStyle & BlockFmt.IND_T:
bFmt.setTextIndent(self._tIndent)
if tType in (BlockTyp.TEXT, BlockTyp.COMMENT, BlockTyp.KEYWORD):
newBlock(cursor, bFmt)
self._insertFragments(tText, tFormat, cursor, self._charFmt)
elif tType in HEADINGS:
bFmt, cFmt = self._genHeadStyle(tType, tMeta, bFmt)
for tPart in tText.split("\n"):
newBlock(cursor, bFmt)
cursor.insertText(tPart, cFmt)
bFmt.setPageBreakPolicy(QtPageBreakAuto)
elif tType == BlockTyp.SEP:
newBlock(cursor, bFmt)
cursor.insertText(tText, self._charFmt)
elif tType == BlockTyp.SKIP:
newBlock(cursor, bFmt)
cursor.insertText(nwUnicode.U_NBSP, self._charFmt)
if tStyle & BlockFmt.PBA:
self._insertNewPageMarker(cursor)
self._document.blockSignals(False)
return
def saveDocument(self, path: Path) -> None:
"""Save the document as a PDF file."""
logger.info("Writing PDF at %d DPI", self._dpi)
printer = QPrinter(QPrinter.PrinterMode.HighResolution)
printer.setDocName(self._project.data.name)
printer.setCreator(f"novelWriter/{__version__}")
printer.setResolution(self._dpi)
printer.setOutputFormat(QPrinter.OutputFormat.PdfFormat)
printer.setPageSize(self._pageSize)
printer.setPageMargins(self._pageMargins, QPageLayout.Unit.Millimeter)
printer.setOutputFileName(str(path))
if layout := self._document.documentLayout():
layout.setPaintDevice(printer)
self._document.setPageSize(printer.pageRect(QPrinter.Unit.DevicePixel).size())
self._document.print(printer)
return
def closeDocument(self) -> None:
"""Run close document tasks."""
self._document.blockSignals(True)
# Replace fields if there are stats available
if self._usedFields and self._counts:
cursor = QTextCursor(self._document)
for pos, field in reversed(self._usedFields):
if (value := self._counts.get(field)) is not None:
cursor.setPosition(pos, QtMoveAnchor)
cursor.setPosition(pos + 1, QtKeepAnchor)
cursor.insertText(self._formatInt(value))
# Add footnotes
if self._usedNotes:
cursor = QTextCursor(self._document)
cursor.movePosition(QTextCursor.MoveOperation.End)
bFmt, cFmt = self._genHeadStyle(BlockTyp.HEAD4, "", self._blockFmt)
newBlock(cursor, bFmt)
cursor.insertText(self._localLookup("Footnotes"), cFmt)
for key, index in self._usedNotes.items():
if content := self._footnotes.get(key):
cFmt = QTextCharFormat(self._charFmt)
cFmt.setForeground(self._theme.code)
cFmt.setAnchor(True)
cFmt.setAnchorNames([f"footnote_{index}"])
newBlock(cursor, self._blockFmt)
cursor.insertText(f"{index}. ", cFmt)
self._insertFragments(*content, cursor, self._charFmt)
self._document.blockSignals(False)
return
##
# Internal Functions
##
def _insertFragments(
self, text: str, tFmt: T_Formats, cursor: QTextCursor, dFmt: QTextCharFormat
) -> None:
"""Apply formatting tags to text."""
cFmt = QTextCharFormat(dFmt)
temp = text.replace("\n", nwUnicode.U_LSEP)
start = 0
primary: QColor | None = None
for pos, fmt, data in tFmt:
# Insert buffer with previous format
cursor.insertText(stripEscape(temp[start:pos]), cFmt)
# Construct next format
if fmt == TextFmt.B_B:
cFmt.setFontWeight(QFont.Weight.Bold)
elif fmt == TextFmt.B_E:
cFmt.setFontWeight(self._dWeight)
elif fmt == TextFmt.I_B:
cFmt.setFontItalic(True)
elif fmt == TextFmt.I_E:
cFmt.setFontItalic(self._dItalic)
elif fmt == TextFmt.D_B:
cFmt.setFontStrikeOut(True)
elif fmt == TextFmt.D_E:
cFmt.setFontStrikeOut(self._dStrike)
elif fmt == TextFmt.U_B:
cFmt.setFontUnderline(True)
elif fmt == TextFmt.U_E:
cFmt.setFontUnderline(self._dUnderline)
elif fmt == TextFmt.M_B:
cFmt.setBackground(self._theme.highlight)
elif fmt == TextFmt.M_E:
cFmt.setBackground(QtTransparent)
elif fmt == TextFmt.SUP_B:
cFmt.setVerticalAlignment(QtVAlignSuper)
elif fmt == TextFmt.SUP_E:
cFmt.setVerticalAlignment(QtVAlignNormal)
elif fmt == TextFmt.SUB_B:
cFmt.setVerticalAlignment(QtVAlignSub)
elif fmt == TextFmt.SUB_E:
cFmt.setVerticalAlignment(QtVAlignNormal)
elif fmt == TextFmt.COL_B:
if color := self._classes.get(data):
cFmt.setForeground(color)
primary = color
elif fmt == TextFmt.COL_E:
cFmt.setForeground(self._theme.text)
primary = None
elif fmt == TextFmt.ANM_B:
if self._anchors:
cFmt.setAnchor(True)
cFmt.setAnchorNames([data])
elif fmt == TextFmt.ANM_E:
if self._anchors:
cFmt.setAnchor(False)
elif fmt == TextFmt.ARF_B:
if self._anchors:
cFmt.setFontUnderline(True)
cFmt.setAnchor(True)
cFmt.setAnchorHref(data)
elif fmt == TextFmt.ARF_E:
if self._anchors:
cFmt.setFontUnderline(False)
cFmt.setAnchor(False)
cFmt.setAnchorHref("")
elif fmt == TextFmt.HRF_B:
cFmt.setForeground(self._theme.link)
cFmt.setFontUnderline(True)
cFmt.setAnchor(True)
cFmt.setAnchorHref(data)
elif fmt == TextFmt.HRF_E:
cFmt.setForeground(primary or self._theme.text)
cFmt.setFontUnderline(self._dUnderline)
cFmt.setAnchor(False)
cFmt.setAnchorHref("")
elif fmt == TextFmt.FNOTE:
xFmt = QTextCharFormat(self._charFmt)
xFmt.setForeground(self._theme.code)
xFmt.setVerticalAlignment(QtVAlignSuper)
if data in self._footnotes:
index = len(self._usedNotes) + 1
self._usedNotes[data] = index
xFmt.setAnchor(True)
xFmt.setAnchorHref(f"#footnote_{index}")
xFmt.setFontUnderline(True)
cursor.insertText(f"[{index}]", xFmt)
else:
cursor.insertText("[ERR]", cFmt)
elif fmt == TextFmt.FIELD:
if field := data.partition(":")[2]:
self._usedFields.append((cursor.position(), field))
cursor.insertText("0", cFmt)
# Move pos for next pass
start = pos
# Insert whatever is left in the buffer
cursor.insertText(stripEscape(temp[start:]), cFmt)
return
def _insertNewPageMarker(self, cursor: QTextCursor) -> None:
"""Insert a new page marker."""
if self._newPage:
bgCol = QColor(self._theme.text)
bgCol.setAlphaF(0.1)
fgCol = QColor(self._theme.text)
fgCol.setAlphaF(0.8)
fFmt = QTextFrameFormat()
fFmt.setBorderStyle(QTextFrameFormat.BorderStyle.BorderStyle_None)
fFmt.setBackground(bgCol)
fFmt.setTopMargin(self._mSep[0])
fFmt.setBottomMargin(self._mSep[1])
bFmt = QTextBlockFormat(self._blockFmt)
bFmt.setAlignment(QtAlignCenter)
bFmt.setTopMargin(0.0)
bFmt.setBottomMargin(0.0)
bFmt.setLineHeight(100.0, QtPropLineHeight)
cFmt = QTextCharFormat(self._charFmt)
cFmt.setFontItalic(False)
cFmt.setFontUnderline(False)
cFmt.setFontStrikeOut(False)
cFmt.setFontWeight(QFont.Weight.Normal)
cFmt.setFontPointSize(0.75*self._textFont.pointSizeF())
cFmt.setForeground(fgCol)
cursor.insertFrame(fFmt)
cursor.setBlockFormat(bFmt)
cursor.insertText(self._project.localLookup("New Page"), cFmt)
if root := self._document.rootFrame():
cursor.swap(root.lastCursorPosition())
return
def _genHeadStyle(self, hType: BlockTyp, hKey: str, rFmt: QTextBlockFormat) -> T_TextStyle:
"""Generate a heading style set."""
mTop, mBottom = self._mHead.get(hType, (0.0, 0.0))
bFmt = QTextBlockFormat(rFmt)
bFmt.setTopMargin(mTop)
bFmt.setBottomMargin(mBottom)
hCol = self._colorHeads and hType != BlockTyp.TITLE
cFmt = QTextCharFormat(self._charFmt)
cFmt.setForeground(self._theme.head if hCol else self._theme.text)
cFmt.setFontWeight(self._hWeight)
cFmt.setFontPointSize(self._sHead.get(hType, 1.0))
if hKey and self._anchors:
cFmt.setAnchorNames([hKey])
cFmt.setAnchor(True)
return bFmt, cFmt