Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix font style and PDF issues #2122

Merged
merged 12 commits into from
Nov 27, 2024
32 changes: 31 additions & 1 deletion novelwriter/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from urllib.request import pathname2url

from PyQt5.QtCore import QCoreApplication, QMimeData, QUrl
from PyQt5.QtGui import QColor, QDesktopServices, QFont, QFontInfo
from PyQt5.QtGui import QColor, QDesktopServices, QFont, QFontDatabase, QFontInfo

from novelwriter.constants import nwConst, nwLabels, nwUnicode, trConst
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
Expand Down Expand Up @@ -434,13 +434,43 @@ def describeFont(font: QFont) -> str:
return "Error"


def fontMatcher(font: QFont) -> QFont:
"""Make sure the font is the correct family, if possible. This
ensures that Qt doesn't re-use another font under the hood. The
default Qt5 font matching algorithm doesn't handle well changing
application fonts at runtime.
"""
info = QFontInfo(font)
if (famRequest := font.family()) != (famActual := info.family()):
logger.warning("Font mismatch: Requested '%s', but got '%s'", famRequest, famActual)
db = QFontDatabase()
if famRequest in db.families():
styleRequest, sizeRequest = font.styleName(), font.pointSize()
logger.info("Lookup: %s, %s, %d pt", famRequest, styleRequest, sizeRequest)
temp = db.font(famRequest, styleRequest, sizeRequest)
temp.setPointSize(sizeRequest) # Make sure it isn't changed
famFound, styleFound, sizeFound = temp.family(), temp.styleName(), temp.pointSize()
if famFound == famRequest:
logger.info("Found: %s, %s, %d pt", famFound, styleFound, sizeFound)
return temp
logger.warning("Could not find a font match in the font database")
logger.warning("If you just changed font, you may need to restart the application")
return font


def qtLambda(func: Callable, *args: Any, **kwargs: Any) -> Callable:
"""A replacement for Python lambdas that works for Qt slots."""
def wrapper(*a_: Any) -> None:
func(*args, **kwargs)
return wrapper


def encodeMimeHandles(mimeData: QMimeData, handles: list[str]) -> None:
"""Encode handles into a mime data object."""
mimeData.setData(nwConst.MIME_HANDLE, b"|".join(h.encode() for h in handles))
return


def decodeMimeHandles(mimeData: QMimeData) -> list[str]:
"""Decode and split a mime data object with handles."""
return mimeData.data(nwConst.MIME_HANDLE).data().decode().split("|")
Expand Down
27 changes: 15 additions & 12 deletions novelwriter/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@
from PyQt5.QtGui import QFont, QFontDatabase
from PyQt5.QtWidgets import QApplication

from novelwriter.common import NWConfigParser, checkInt, checkPath, describeFont, formatTimeStamp
from novelwriter.common import (
NWConfigParser, checkInt, checkPath, describeFont, fontMatcher,
formatTimeStamp
)
from novelwriter.constants import nwFiles, nwUnicode
from novelwriter.error import formatException, logException

Expand Down Expand Up @@ -369,10 +372,11 @@ def setBackupPath(self, path: Path | str) -> None:
def setGuiFont(self, value: QFont | str | None) -> None:
"""Update the GUI's font style from settings."""
if isinstance(value, QFont):
self.guiFont = value
self.guiFont = fontMatcher(value)
elif value and isinstance(value, str):
self.guiFont = QFont()
self.guiFont.fromString(value)
font = QFont()
font.fromString(value)
self.guiFont = fontMatcher(font)
else:
font = QFont()
fontDB = QFontDatabase()
Expand All @@ -382,22 +386,21 @@ def setGuiFont(self, value: QFont | str | None) -> None:
font.setPointSize(10)
else:
font = fontDB.systemFont(QFontDatabase.SystemFont.GeneralFont)
self.guiFont = font
self.guiFont = fontMatcher(font)
logger.debug("GUI font set to: %s", describeFont(font))

QApplication.setFont(self.guiFont)

return

def setTextFont(self, value: QFont | str | None) -> None:
"""Set the text font if it exists. If it doesn't, or is None,
set to default font.
"""
if isinstance(value, QFont):
self.textFont = value
self.textFont = fontMatcher(value)
elif value and isinstance(value, str):
self.textFont = QFont()
self.textFont.fromString(value)
font = QFont()
font.fromString(value)
self.textFont = fontMatcher(font)
else:
fontDB = QFontDatabase()
fontFam = fontDB.families()
Expand All @@ -411,8 +414,8 @@ def setTextFont(self, value: QFont | str | None) -> None:
font.setPointSize(12)
else:
font = fontDB.systemFont(QFontDatabase.SystemFont.GeneralFont)
self.textFont = font
logger.debug("Text font set to: %s", describeFont(font))
self.textFont = fontMatcher(font)
logger.debug("Text font set to: %s", describeFont(self.textFont))
return

##
Expand Down
4 changes: 2 additions & 2 deletions novelwriter/core/docbuild.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def iterBuildDocument(self, path: Path, bFormat: nwBuildFmt) -> Iterable[tuple[i
makeObj = ToQTextDocument(self._project)
makeObj.disableAnchors()
filtered = self._setupBuild(makeObj)
makeObj.initDocument()
makeObj.initDocument(pdf=True)
yield from self._iterBuild(makeObj, filtered)
makeObj.closeDocument()

Expand Down Expand Up @@ -218,7 +218,7 @@ def _setupBuild(self, bldObj: Tokenizer) -> dict:
textFont = QFont(CONFIG.textFont)
textFont.fromString(self._build.getStr("format.textFont"))

bldObj.setFont(textFont)
bldObj.setTextFont(textFont)
bldObj.setLanguage(self._project.data.language)

bldObj.setPartitionFormat(
Expand Down
6 changes: 3 additions & 3 deletions novelwriter/core/itemmodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from PyQt5.QtCore import QAbstractItemModel, QMimeData, QModelIndex, Qt
from PyQt5.QtGui import QFont, QIcon

from novelwriter.common import decodeMimeHandles, minmax
from novelwriter.common import decodeMimeHandles, encodeMimeHandles, minmax
from novelwriter.constants import nwConst
from novelwriter.core.item import NWItem
from novelwriter.enum import nwItemClass
Expand Down Expand Up @@ -367,11 +367,11 @@ def mimeTypes(self) -> list[str]:
def mimeData(self, indices: list[QModelIndex]) -> QMimeData:
"""Encode mime data about a selection."""
handles = [
i.internalPointer().item.itemHandle.encode()
i.internalPointer().item.itemHandle
for i in indices if i.isValid() and i.column() == 0
]
mime = QMimeData()
mime.setData(nwConst.MIME_HANDLE, b"|".join(handles))
encodeMimeHandles(mime, handles)
return mime

def canDropMimeData(
Expand Down
6 changes: 3 additions & 3 deletions novelwriter/formats/tokenizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from PyQt5.QtGui import QColor, QFont

from novelwriter import CONFIG
from novelwriter.common import checkInt, numberToRoman
from novelwriter.common import checkInt, fontMatcher, numberToRoman
from novelwriter.constants import (
nwHeadFmt, nwKeyWords, nwLabels, nwShortcode, nwStats, nwStyles, nwUnicode,
trConst
Expand Down Expand Up @@ -302,9 +302,9 @@ def setSceneStyle(self, center: bool, pageBreak: bool) -> None:
self._sceneStyle |= BlockFmt.PBB if pageBreak else BlockFmt.NONE
return

def setFont(self, font: QFont) -> None:
def setTextFont(self, font: QFont) -> None:
"""Set the build font."""
self._textFont = font
self._textFont = fontMatcher(font)
return

def setLineHeight(self, height: float) -> None:
Expand Down
8 changes: 1 addition & 7 deletions novelwriter/formats/toodt.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
from novelwriter.core.project import NWProject
from novelwriter.formats.shared import BlockFmt, BlockTyp, TextFmt, stripEscape
from novelwriter.formats.tokenizer import Tokenizer
from novelwriter.types import FONT_STYLE, FONT_WEIGHTS, QtHexRgb
from novelwriter.types import FONT_STYLE, QtHexRgb

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -220,20 +220,14 @@ def initDocument(self) -> None:
# Initialise Variables
# ====================

intWeight = FONT_WEIGHTS.get(self._textFont.weight(), 400)
fontWeight = str(intWeight)
fontBold = str(min(intWeight + 300, 900))

lang, _, country = self._dLocale.name().partition("_")
self._dLanguage = lang or self._dLanguage
self._dCountry = country or self._dCountry

self._fontFamily = self._textFont.family()
self._fontSize = self._textFont.pointSize()
self._fontWeight = FONT_WEIGHT_MAP.get(fontWeight, fontWeight)
self._fontStyle = FONT_STYLE.get(self._textFont.style(), "normal")
self._fontPitch = "fixed" if self._textFont.fixedPitch() else "variable"
self._fontBold = FONT_WEIGHT_MAP.get(fontBold, fontBold)
self._headWeight = self._fontBold if self._boldHeads else None
self._fBlockIndent = self._emToCm(self._blockIndent)

Expand Down
83 changes: 59 additions & 24 deletions novelwriter/formats/toqdoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@

from PyQt5.QtCore import QMarginsF, QSizeF
from PyQt5.QtGui import (
QColor, QFont, QPageSize, QTextBlockFormat, QTextCharFormat, QTextCursor,
QTextDocument
QColor, QFont, QFontDatabase, QPageSize, QTextBlockFormat, QTextCharFormat,
QTextCursor, QTextDocument, QTextFrameFormat
)
from PyQt5.QtPrintSupport import QPrinter

Expand Down Expand Up @@ -74,11 +74,16 @@ def __init__(self, project: NWProject) -> None:
self._usedFields: list[tuple[int, str]] = []

self._init = False
self._bold = QFont.Weight.Bold
self._normal = QFont.Weight.Normal
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)

Expand Down Expand Up @@ -119,21 +124,37 @@ def disableAnchors(self) -> None:
# Class Methods
##

def initDocument(self) -> None:
def initDocument(self, pdf: bool = False) -> None:
"""Initialise all computed values of the document."""
super().initDocument()

if pdf:
fontDB = QFontDatabase()
family = self._textFont.family()
style = self._textFont.styleName()
self._dpi = 1200 if fontDB.isScalable(family, style) else 72

self._document.setUndoRedoEnabled(False)
self._document.blockSignals(True)
self._document.clear()
self._document.setDefaultFont(self._textFont)

fPt = self._textFont.pointSizeF()
fPx = fPt*96.0/72.0 # 1 em in pixels
# 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.HEAD1: (fPx * self._marginHead1[0], fPx * self._marginHead1[1]),
Expand All @@ -155,8 +176,8 @@ def initDocument(self) -> None:
self._mMeta = (fPx * self._marginMeta[0], fPx * self._marginMeta[1])
self._mSep = (fPx * self._marginSep[0], fPx * self._marginSep[1])

self._mIndent = fPx * 2.0
self._tIndent = fPx * self._firstWidth
self._mIndent = mPx * 2.0
self._tIndent = mPx * self._firstWidth

# Text Formats
# ============
Expand Down Expand Up @@ -248,10 +269,12 @@ def doConvert(self) -> None:
def saveDocument(self, path: Path) -> None:
"""Save the document as a PDF file."""
m = self._pageMargins
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(m.left(), m.top(), m.right(), m.bottom(), QPrinter.Unit.Millimeter)
Expand Down Expand Up @@ -318,21 +341,21 @@ def _insertFragments(

# Construct next format
if fmt == TextFmt.B_B:
cFmt.setFontWeight(self._bold)
cFmt.setFontWeight(QFont.Weight.Bold)
elif fmt == TextFmt.B_E:
cFmt.setFontWeight(self._normal)
cFmt.setFontWeight(self._dWeight)
elif fmt == TextFmt.I_B:
cFmt.setFontItalic(True)
elif fmt == TextFmt.I_E:
cFmt.setFontItalic(False)
cFmt.setFontItalic(self._dItalic)
elif fmt == TextFmt.D_B:
cFmt.setFontStrikeOut(True)
elif fmt == TextFmt.D_E:
cFmt.setFontStrikeOut(False)
cFmt.setFontStrikeOut(self._dStrike)
elif fmt == TextFmt.U_B:
cFmt.setFontUnderline(True)
elif fmt == TextFmt.U_E:
cFmt.setFontUnderline(False)
cFmt.setFontUnderline(self._dUnderline)
elif fmt == TextFmt.M_B:
cFmt.setBackground(self._theme.highlight)
elif fmt == TextFmt.M_E:
Expand Down Expand Up @@ -376,7 +399,7 @@ def _insertFragments(
cFmt.setAnchorHref(data)
elif fmt == TextFmt.HRF_E:
cFmt.setForeground(primary or self._theme.text)
cFmt.setFontUnderline(False)
cFmt.setFontUnderline(self._dUnderline)
cFmt.setAnchor(False)
cFmt.setAnchorHref("")
elif fmt == TextFmt.FNOTE:
Expand Down Expand Up @@ -409,24 +432,36 @@ def _insertFragments(
def _insertNewPageMarker(self, cursor: QTextCursor) -> None:
"""Insert a new page marker."""
if self._newPage:
cursor.insertHtml("<hr width='100%'>")
bgCol = QColor(self._theme.text)
bgCol.setAlphaF(0.1)
fgCol = QColor(self._theme.text)
fgCol.setAlphaF(0.8)

hFmt = cursor.blockFormat()
hFmt.setBottomMargin(0.0)
hFmt.setLineHeight(75.0, QtPropLineHeight)
cursor.setBlockFormat(hFmt)
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.setLineHeight(75.0, QtPropLineHeight)
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(self._theme.comment)
cFmt.setForeground(fgCol)

newBlock(cursor, bFmt)
cursor.insertFrame(fFmt)
cursor.setBlockFormat(bFmt)
cursor.insertText(self._project.localLookup("New Page"), cFmt)
cursor.swap(self._document.rootFrame().lastCursorPosition())

return

def _genHeadStyle(self, hType: BlockTyp, hKey: str, rFmt: QTextBlockFormat) -> T_TextStyle:
Expand All @@ -440,7 +475,7 @@ def _genHeadStyle(self, hType: BlockTyp, hKey: str, rFmt: QTextBlockFormat) -> T
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._bold if self._boldHeads else self._normal)
cFmt.setFontWeight(self._hWeight)
cFmt.setFontPointSize(self._sHead.get(hType, 1.0))
if hKey and self._anchors:
cFmt.setAnchorNames([hKey])
Expand Down
Loading