Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,10 @@ Example for linux font file and directory paths:
```
#### Missing-font substitutions

Family names are resolved to their loaded faces before missing-font substitution.
If a family has no regular face, an available italic, bold, or bold italic face
is used, in that order. No manual substitution is needed for such a family.

To choose a replacement when a requested font is unavailable, add name pairs to
`cewe2pdf.ini`:

Expand Down Expand Up @@ -323,9 +327,9 @@ the MCF also supplies a positive width and a non-transparent colour.

#### Letter spacing

The area-wide `textFormat.letterSpacing` setting is supported. Per-span letter
spacing and wrapping-aware measurement are not implemented, so heavily spaced
text near the edge of a narrow frame deserves visual checking.
The area-wide `textFormat.letterSpacing` setting is supported, including line
breaking and horizontal alignment. Positive spacing can move words to the next
line; negative spacing can let more text fit. Per-span letter spacing is not implemented.

#### Tabs

Expand Down
25 changes: 18 additions & 7 deletions fonts/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os

from fontTools import ttLib
from reportlab.lib.fonts import tt2ps
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont

Expand Down Expand Up @@ -397,11 +398,21 @@ def noteFontSubstitution(family, replacement, state: ConversionState):

def getAvailableFont(family, pdf, additional_fonts, state: ConversionState):
reportlabFonts = pdf.getAvailableFonts()
if family in reportlabFonts:
bodyfont = family
elif family in additional_fonts:
bodyfont = family
else:
bodyfont = getMissingFontSubstitute(family, state)
noteFontSubstitution(family, bodyfont, state)
if family in reportlabFonts or family in additional_fonts:
return family

# CEWE names families, whereas the available-font lists contain faces.
# A family with only an italic/bold face still has a usable normal mapping
# from registerFontFamilies. Resolve it before declaring the font missing.
try:
normalFace = tt2ps(family, 0, 0)
except ValueError:
normalFace = None
# ReportLab keeps family mappings across conversions; accept this alias
# only when its face was loaded for the current album.
if normalFace in additional_fonts:
return normalFace

bodyfont = getMissingFontSubstitute(family, state)
noteFontSubstitution(family, bodyfont, state)
return bodyfont
69 changes: 69 additions & 0 deletions tests/testFontSubstitution/test_fontFamilyResolution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Resolve CEWE family names whose only installed face is not regular."""

from io import BytesIO
from pathlib import Path
import sys

import pytest
from reportlab.lib import fonts
from reportlab.lib.styles import ParagraphStyle
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import Paragraph

PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from testutils import configureTestImportPaths
configureTestImportPaths(__file__)

from conversionState import ConversionState
from fonts.manager import getAvailableFont, registerFontFamilies


@pytest.fixture
def isolatedFontRegistry(monkeypatch):
# ReportLab's mappings are process-wide. Test registrations must not
# affect later album conversions or depend on earlier test registrations.
for module, name in ((pdfmetrics, '_fonts'), (pdfmetrics, '_dynFaceNames'),
(fonts, '_tt2ps_map'), (fonts, '_ps2tt_map')):
monkeypatch.setattr(module, name, getattr(module, name).copy())


@pytest.mark.parametrize('onlyStyle', ['italic', 'bold', 'boldItalic'])
def test_familyWithNoRegularFaceUsesChosenAlternate(isolatedFontRegistry, onlyStyle):
family = f'Fallback test family {onlyStyle}'
face = f'{family} face'
fontFile = PROJECT_ROOT / 'tests/Resources/photofun/fonts/LiberationSansItalic-RJre.ttf'
pdfmetrics.registerFont(TTFont(face, str(fontFile)))
members = dict.fromkeys(('normal', 'bold', 'italic', 'boldItalic'))
members[onlyStyle] = face
registerFontFamilies({family: members}, [])
pdf = Canvas(BytesIO())
state = ConversionState()
# Even an explicit missing-font rule must not override an available family.
state.missing_font_substitutions[family] = 'Courier'

selectedFace = getAvailableFont(family, pdf, {face: str(fontFile)}, state)

assert selectedFace == face
assert state.noted_font_substitutions == set()
paragraph = Paragraph('Normal <b>bold</b> <i>italic</i> <b><i>both</i></b>',
ParagraphStyle('family fallback', fontName=selectedFace))
assert all(fragment.fontName == face for fragment in paragraph.frags)
paragraph.wrap(300, 100)
paragraph.drawOn(pdf, 10, 10)
pdf.save()


def test_familyMappingFromEarlierConversionIsNotEnough(isolatedFontRegistry):
family = 'Unavailable family from earlier conversion'
pdfmetrics.registerFontFamily(family, normal='Courier')
state = ConversionState()

assert getAvailableFont(family, Canvas(BytesIO()), {}, state) == 'Helvetica'
assert state.noted_font_substitutions == {f'{family}/Helvetica'}


if __name__ == '__main__':
sys.exit(pytest.main([__file__]))
90 changes: 90 additions & 0 deletions tests/testLetterSpacing/test_spacingLayout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""Measure line breaks and alignment with PDF character spacing enabled."""

from io import BytesIO
from pathlib import Path
import sys

import pytest
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.enums import TA_CENTER
from reportlab.pdfbase.pdfmetrics import stringWidth
from reportlab.pdfgen.canvas import Canvas

PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from testutils import configureTestImportPaths
configureTestImportPaths(__file__)

from texts.outlines import TextEffectsParagraph


def makeParagraph(text, spacing):
style = ParagraphStyle('spacing test', fontName='Helvetica', fontSize=36, leading=42,
alignment=TA_CENTER, splitLongWords=True)
style.letterSpacing = spacing
return TextEffectsParagraph(text, style)


def lineTexts(paragraph):
return [''.join(fragment.text for fragment in line.words) for line in paragraph.blPara.lines]


def test_spacedTextWrapsAndCentresUsingVisibleWidth():
paragraph = makeParagraph('Text with shadow', 10)
paragraph.wrap(330, 500)
assert lineTexts(paragraph) == ['Text with', 'shadow']
for line in paragraph.blPara.lines:
text = ''.join(fragment.text for fragment in line.words)
visibleWidth = stringWidth(text, 'Helvetica', 36) + (len(text) - 1) * 10
assert line.extraSpace == pytest.approx(330 - visibleWidth)
assert paragraph.height == 84
paragraph.wrap(330, 500)
assert lineTexts(paragraph) == ['Text with', 'shadow']
paragraph.wrap(700, 500)
assert lineTexts(paragraph) == ['Text with shadow']


def test_negativeSpacingAllowsMoreTextOnTheLine():
paragraph = makeParagraph('Text with shadow', -1)
width = stringWidth('Text with shadow', 'Helvetica', 36) - 5
paragraph.wrap(width, 500)
assert lineTexts(paragraph) == ['Text with shadow']


def test_inlineStylesAndExplicitBreaksArePreserved():
paragraph = makeParagraph('Text <b>with</b> shadow<br/>Next <i>line</i>', 10)
paragraph.wrap(330, 500)
assert lineTexts(paragraph) == ['Text with', 'shadow', 'Next line']
assert [fragment.fontName for fragment in paragraph.blPara.lines[0].words if fragment.text.strip()] == [
'Helvetica', 'Helvetica-Bold']
assert paragraph.blPara.lines[1].lineBreak
pdf = Canvas(BytesIO())
paragraph.drawOn(pdf, 20, 20)
pdf.save()


def test_overlongWordSplitsWithoutLosingStyleOrCharacters():
paragraph = makeParagraph('abc<b>defghij</b>', 10)
paragraph.wrap(100, 500)
assert ''.join(lineTexts(paragraph)) == 'abcdefghij'
assert len(paragraph.blPara.lines) > 1
assert all(line.extraSpace >= 0 for line in paragraph.blPara.lines)
assert paragraph.blPara.lines[-1].words[-1].fontName == 'Helvetica-Bold'


def test_existingHyphenRemainsAnAvailableBreak():
paragraph = makeParagraph('Text-with-shadow', 10)
paragraph.style.embeddedHyphenation = 1
paragraph.wrap(330, 500)
assert lineTexts(paragraph) == ['Text-with-', 'shadow']


def test_zeroWidthSpaceAdvanceStillPreservesTheSpace():
spacing = -stringWidth(' ', 'Helvetica', 36)
paragraph = makeParagraph('abc def', spacing)
paragraph.wrap(500, 500)
assert lineTexts(paragraph) == ['abc def']


if __name__ == '__main__':
sys.exit(pytest.main([__file__]))
4 changes: 3 additions & 1 deletion tests/testTextBox/cewe2pdf.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
[DEFAULT]
#cewe_folder = C:\Program Files\Elkjop fotoservice_6.3\elkjop fotoservice
#cewe_folder = C:\Program Files\Elkjop fotoservice\elkjop fotoservice
#noShadows = False
#loadSystemFonts = True

cewe_folder = tests
hpsFolder = tests/hps
Binary file not shown.
Loading
Loading