Add PDF output for manuscripts (#2048)

This commit is contained in:
Veronica Berglyd Olsen
2024-10-16 17:41:31 +02:00
committed by GitHub
8 changed files with 118 additions and 86 deletions
+2
View File
@@ -250,6 +250,7 @@ class nwLabels:
nwBuildFmt.NWD: QT_TRANSLATE_NOOP("Constant", "novelWriter Markup (.txt)"),
nwBuildFmt.STD_MD: QT_TRANSLATE_NOOP("Constant", "Standard Markdown (.md)"),
nwBuildFmt.EXT_MD: QT_TRANSLATE_NOOP("Constant", "Extended Markdown (.md)"),
nwBuildFmt.PDF: QT_TRANSLATE_NOOP("Constant", "Portable Document Format (.pdf)"),
nwBuildFmt.J_HTML: QT_TRANSLATE_NOOP("Constant", "JSON + novelWriter HTML (.json)"),
nwBuildFmt.J_NWD: QT_TRANSLATE_NOOP("Constant", "JSON + novelWriter Markup (.json)"),
}
@@ -260,6 +261,7 @@ class nwLabels:
nwBuildFmt.NWD: ".txt",
nwBuildFmt.STD_MD: ".md",
nwBuildFmt.EXT_MD: ".md",
nwBuildFmt.PDF: ".pdf",
nwBuildFmt.J_HTML: ".json",
nwBuildFmt.J_NWD: ".json",
}
+21 -13
View File
@@ -41,7 +41,7 @@ from novelwriter.formats.tohtml import ToHtml
from novelwriter.formats.tokenizer import Tokenizer
from novelwriter.formats.tomarkdown import ToMarkdown
from novelwriter.formats.toodt import ToOdt
from novelwriter.formats.toqdoc import TextDocumentTheme, ToQTextDocument
from novelwriter.formats.toqdoc import ToQTextDocument
from novelwriter.formats.toraw import ToRaw
logger = logging.getLogger(__name__)
@@ -121,24 +121,15 @@ class NWBuildDocument:
self._queue.append(item.itemHandle)
return
def iterBuildPreview(self, theme: TextDocumentTheme) -> Iterable[tuple[int, bool]]:
def iterBuildPreview(self) -> Iterable[tuple[int, bool]]:
"""Build a preview QTextDocument."""
makeObj = ToQTextDocument(self._project)
filtered = self._setupBuild(makeObj)
makeObj.initDocument()
self._count = True
self._outline = True
font = QFont()
font.fromString(self._build.getStr("format.textFont"))
makeObj.initDocument(font, theme)
for i, tHandle in enumerate(self._queue):
self._error = None
if filtered.get(tHandle, (False, 0))[0]:
yield i, self._doBuild(makeObj, tHandle)
else:
yield i, False
yield from self._iterBuild(makeObj, filtered)
makeObj.appendFootnotes()
@@ -149,6 +140,9 @@ class NWBuildDocument:
def iterBuildDocument(self, path: Path, bFormat: nwBuildFmt) -> Iterable[tuple[int, bool]]:
"""Wrapper for builders based on format."""
self._error = None
self._cache = None
if bFormat in (nwBuildFmt.J_HTML, nwBuildFmt.J_NWD):
# Ensure that JSON output has the correct extension
path = path.with_suffix(".json")
@@ -191,6 +185,19 @@ class NWBuildDocument:
if self._build.getBool("format.replaceTabs"):
makeObj.replaceTabs(nSpaces=4, spaceChar=" ")
elif bFormat == nwBuildFmt.PDF:
makeObj = ToQTextDocument(self._project)
filtered = self._setupBuild(makeObj)
makeObj.initDocument()
yield from self._iterBuild(makeObj, filtered)
makeObj.appendFootnotes()
else:
logger.error("Unsupported document format")
return
self._error = None
self._cache = makeObj
@@ -297,6 +304,7 @@ class NWBuildDocument:
self._build.getInt("odt.pageCountOffset"),
)
if isinstance(bldObj, (ToOdt, 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(
+3 -2
View File
@@ -184,8 +184,9 @@ class nwBuildFmt(Enum):
NWD = 3
STD_MD = 4
EXT_MD = 5
J_HTML = 6
J_NWD = 7
PDF = 6
J_HTML = 7
J_NWD = 8
class nwStatusShape(Enum):
+63 -33
View File
@@ -27,18 +27,20 @@ import logging
from pathlib import Path
from PyQt5.QtCore import QMarginsF, QSizeF
from PyQt5.QtGui import (
QColor, QFont, QFontMetricsF, QTextBlockFormat, QTextCharFormat,
QColor, QFont, QFontMetricsF, QPageSize, QTextBlockFormat, QTextCharFormat,
QTextCursor, QTextDocument
)
from PyQt5.QtPrintSupport import QPrinter
from novelwriter.constants import nwHeaders, nwHeadFmt, nwKeyWords, nwLabels, nwUnicode
from novelwriter.core.project import NWProject
from novelwriter.formats.tokenizer import T_Formats, Tokenizer
from novelwriter.types import (
QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight,
QtBlack, QtPageBreakAfter, QtPageBreakBefore, QtTransparent,
QtVAlignNormal, QtVAlignSub, QtVAlignSuper
QtPageBreakAfter, QtPageBreakBefore, QtTransparent, QtVAlignNormal,
QtVAlignSub, QtVAlignSuper
)
logger = logging.getLogger(__name__)
@@ -47,18 +49,18 @@ T_TextStyle = tuple[QTextBlockFormat, QTextCharFormat]
class TextDocumentTheme:
text: QColor = QtBlack
highlight: QColor = QtTransparent
head: QColor = QtBlack
comment: QColor = QtBlack
note: QColor = QtBlack
code: QColor = QtBlack
modifier: QColor = QtBlack
keyword: QColor = QtBlack
tag: QColor = QtBlack
optional: QColor = QtBlack
dialog: QColor = QtBlack
altdialog: QColor = QtBlack
text: QColor = QColor(0, 0, 0)
highlight: QColor = QColor(255, 255, 166)
head: QColor = QColor(66, 113, 174)
comment: QColor = QColor(100, 100, 100)
note: QColor = QColor(129, 55, 9)
code: QColor = QColor(66, 113, 174)
modifier: QColor = QColor(129, 55, 9)
keyword: QColor = QColor(245, 135, 31)
tag: QColor = QColor(66, 113, 174)
optional: QColor = QColor(66, 113, 174)
dialog: QColor = QColor(66, 113, 174)
altdialog: QColor = QColor(129, 55, 9)
def newBlock(cursor: QTextCursor, bFmt: QTextBlockFormat) -> None:
@@ -89,13 +91,43 @@ class ToQTextDocument(Tokenizer):
self._bold = QFont.Weight.Bold
self._normal = QFont.Weight.Normal
self._pageSize = QPageSize(QPageSize.PageSizeId.A4)
self._pageMargins = QMarginsF(20.0, 20.0, 20.0, 20.0)
return
def initDocument(self, font: QFont, theme: TextDocumentTheme) -> None:
"""Initialise all computed values of the document."""
self._textFont = font
self._theme = theme
##
# Properties
##
@property
def document(self) -> QTextDocument:
"""Return the document."""
return self._document
##
# Setters
##
def setTheme(self, theme: TextDocumentTheme) -> None:
"""Set the document colour theme."""
self._theme = theme
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."""
self._pageSize = QPageSize(QSizeF(width, height), QPageSize.Unit.Millimeter)
self._pageMargins = QMarginsF(left, top, right, bottom)
return
##
# Class Methods
##
def initDocument(self) -> None:
"""Initialise all computed values of the document."""
self._document.setUndoRedoEnabled(False)
self._document.blockSignals(True)
self._document.clear()
@@ -180,19 +212,6 @@ class ToQTextDocument(Tokenizer):
return
##
# Properties
##
@property
def document(self) -> QTextDocument:
"""Return the document."""
return self._document
##
# Class Methods
##
def doConvert(self) -> None:
"""Write text tokens into the document."""
if not self._init:
@@ -276,7 +295,18 @@ class ToQTextDocument(Tokenizer):
return
def saveDocument(self, path: Path) -> None:
"""Not implemented."""
"""Save the document to a PDF file."""
m = self._pageMargins
printer = QPrinter(QPrinter.PrinterMode.PrinterResolution)
printer.setOutputFormat(QPrinter.OutputFormat.PdfFormat)
printer.setPageSize(self._pageSize)
printer.setPageMargins(m.left(), m.top(), m.right(), m.bottom(), QPrinter.Unit.Millimeter)
printer.setOutputFileName(str(path))
self._document.setPageSize(self._pageSize.size(QPageSize.Unit.Point))
self._document.print(printer)
return
def appendFootnotes(self) -> None:
+3 -1
View File
@@ -211,7 +211,9 @@ class GuiDocViewer(QTextBrowser):
qDoc = ToQTextDocument(SHARED.project)
qDoc.setJustify(CONFIG.doJustify)
qDoc.setDialogueHighlight(True)
qDoc.initDocument(CONFIG.textFont, self._docTheme)
qDoc.setFont(CONFIG.textFont)
qDoc.setTheme(self._docTheme)
qDoc.initDocument()
qDoc.setKeywords(True)
qDoc.setComments(CONFIG.viewComments)
qDoc.setSynopsis(CONFIG.viewSynopsis)
+2 -16
View File
@@ -45,7 +45,7 @@ from novelwriter.core.docbuild import NWBuildDocument
from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton, NToolDialog
from novelwriter.extensions.progressbars import NProgressCircle
from novelwriter.formats.tokenizer import HeadingFormatter
from novelwriter.formats.toqdoc import TextDocumentTheme, ToQTextDocument
from novelwriter.formats.toqdoc import ToQTextDocument
from novelwriter.gui.theme import STYLES_FLAT_TABS, STYLES_MIN_TOOLBUTTON
from novelwriter.tools.manusbuild import GuiManuscriptBuild
from novelwriter.tools.manussettings import GuiBuildSettings
@@ -330,22 +330,8 @@ class GuiManuscript(NToolDialog):
docBuild = NWBuildDocument(SHARED.project, build)
docBuild.queueAll()
theme = TextDocumentTheme()
theme.text = QColor(0, 0, 0)
theme.highlight = QColor(255, 255, 166)
theme.head = QColor(66, 113, 174)
theme.comment = QColor(100, 100, 100)
theme.note = QColor(129, 55, 9)
theme.code = QColor(66, 113, 174)
theme.modifier = QColor(129, 55, 9)
theme.keyword = QColor(245, 135, 31)
theme.tag = QColor(66, 113, 174)
theme.optional = QColor(66, 113, 174)
theme.dialog = QColor(66, 113, 174)
theme.altdialog = QColor(129, 55, 9)
self.docPreview.beginNewBuild(len(docBuild))
for step, _ in docBuild.iterBuildPreview(theme):
for step, _ in docBuild.iterBuildPreview():
self.docPreview.buildStep(step + 1)
QApplication.processEvents()
+15
View File
@@ -34,6 +34,7 @@ from novelwriter.enum import nwBuildFmt
from novelwriter.formats.tohtml import ToHtml
from novelwriter.formats.tomarkdown import ToMarkdown
from novelwriter.formats.toodt import ToOdt
from novelwriter.formats.toqdoc import ToQTextDocument
from novelwriter.formats.toraw import ToRaw
from tests.mocked import causeException, causeOSError
@@ -588,3 +589,17 @@ def testCoreDocBuild_IterBuild(mockGUI, fncPath: Path, mockRnd):
assert "meta" in data
assert "text" in data
docFile.unlink()
# PDF Format
docFile = fncPath / "Minimal.pdf"
assert list(docBuild.iterBuildDocument(docFile, nwBuildFmt.PDF)) == [
(0, True), (1, True), (2, False), (3, True), (4, True),
(5, True), (6, True), (7, True), (8, True), (9, False),
]
assert isinstance(docBuild.lastBuild, ToQTextDocument)
assert docFile.is_file()
docFile.unlink()
# Invalid Format
assert list(docBuild.iterBuildDocument(docFile, None)) == [] # type: ignore
assert docBuild.lastBuild is None
+9 -21
View File
@@ -22,7 +22,7 @@ from __future__ import annotations
import pytest
from PyQt5.QtGui import QColor, QTextBlock, QTextCharFormat, QTextCursor
from PyQt5.QtGui import QTextBlock, QTextCharFormat, QTextCursor
from novelwriter import CONFIG
from novelwriter.constants import nwUnicode
@@ -34,18 +34,6 @@ from novelwriter.types import (
)
THEME = TextDocumentTheme()
THEME.text = QColor(0, 0, 0)
THEME.highlight = QColor(255, 255, 166)
THEME.head = QColor(66, 113, 174)
THEME.comment = QColor(100, 100, 100)
THEME.note = QColor(129, 55, 9)
THEME.code = QColor(66, 113, 174)
THEME.modifier = QColor(129, 55, 9)
THEME.keyword = QColor(245, 135, 31)
THEME.tag = QColor(66, 113, 174)
THEME.optional = QColor(66, 113, 174)
THEME.dialog = QColor(113, 140, 0)
THEME.altdialog = QColor(234, 183, 0)
def charFmtInBlock(block: QTextBlock, pos: int) -> QTextCharFormat:
@@ -60,8 +48,8 @@ def testFmtToQTextDocument_ConvertHeaders(mockGUI):
"""Test header formats in the ToQTextDocument class."""
project = NWProject()
qdoc = ToQTextDocument(project)
qdoc.initDocument(CONFIG.textFont, THEME)
qdoc.saveDocument("") # Doesn't do anything for this format
qdoc.initDocument()
# qdoc.saveDocument("") # Doesn't do anything for this format
qdoc._isNovel = True
qdoc._isFirst = True
@@ -137,7 +125,7 @@ def testFmtToQTextDocument_SeparatorSkip(mockGUI):
"""Test separator and skip in the ToQTextDocument class."""
project = NWProject()
qdoc = ToQTextDocument(project)
qdoc.initDocument(CONFIG.textFont, THEME)
qdoc.initDocument()
qdoc._isNovel = True
qdoc._isFirst = True
@@ -203,7 +191,7 @@ def testFmtToQTextDocument_NovelMeta(mockGUI):
"""Test novel meta formats in the ToQTextDocument class."""
project = NWProject()
qdoc = ToQTextDocument(project)
qdoc.initDocument(CONFIG.textFont, THEME)
qdoc.initDocument()
qdoc._isNovel = True
qdoc._isFirst = True
@@ -280,7 +268,7 @@ def testFmtToQTextDocument_NoteMeta(mockGUI):
"""Test note meta formats in the ToQTextDocument class."""
project = NWProject()
qdoc = ToQTextDocument(project)
qdoc.initDocument(CONFIG.textFont, THEME)
qdoc.initDocument()
qdoc._isNovel = False
qdoc._isFirst = True
@@ -338,7 +326,7 @@ def testFmtToQTextDocument_TextBlockFormats(mockGUI):
project = NWProject()
qdoc = ToQTextDocument(project)
qdoc.setFirstLineIndent(True, 2.0, False)
qdoc.initDocument(CONFIG.textFont, THEME)
qdoc.initDocument()
qdoc._isNovel = True
qdoc._isFirst = True
@@ -460,7 +448,7 @@ def testFmtToQTextDocument_TextCharFormats(mockGUI):
assert qdoc.document.toPlainText() == ""
# Init
qdoc.initDocument(CONFIG.textFont, THEME)
qdoc.initDocument()
qdoc._isNovel = True
qdoc._isFirst = True
@@ -581,7 +569,7 @@ def testFmtToQTextDocument_Footnotes(mockGUI):
"""Test footnotes in the ToQTextDocument class."""
project = NWProject()
qdoc = ToQTextDocument(project)
qdoc.initDocument(CONFIG.textFont, THEME)
qdoc.initDocument()
qdoc._isNovel = True
qdoc._isFirst = True