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 weights #919

Merged
merged 15 commits into from
Mar 29, 2022
Merged
Show file tree
Hide file tree
Changes from 14 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
18 changes: 13 additions & 5 deletions docs/source/kiva/drawing_details.rst
Original file line number Diff line number Diff line change
Expand Up @@ -300,11 +300,15 @@ can be passed to the :py:meth:`~.AbstractGraphicsContext.set_font` method.
~~~~~~~~~~~~~~~~

If you don't want to rely on the font description parsing in ``KivaFont``, you
can also manually construct a :class:`kiva.fonttools.font.Font` instance. Once
can also manually construct a :class:`~kiva.fonttools.font.Font` instance. Once
you have a ``Font`` instance, it can be passed to the
:py:meth:`~.AbstractGraphicsContext.set_font` method.
:py:meth:`~.AbstractGraphicsContext.set_font` method. Note that
:class:`~kiva.fonttools.font.Font` is an expression of the *desired* font.
The actual font that is rendererd depends on the capabilities of the Kiva
backend, the operating system, and the fonts actually installed on the user's
system.

``Font(face_name="", size=12, family=SWISS, weight=NORMAL, style=NORMAL)``
``Font(face_name="", size=12, family=SWISS, weight=WEIGHT_NORMAL, style=NORMAL)``

``face_name`` is the font's name: "Arial", "Webdings", "Verdana", etc.

Expand All @@ -315,8 +319,12 @@ you have a ``Font`` instance, it can be passed to the
If ``face_name`` is empty, the value of ``family`` will be used to select the
desired font.

``weight`` is a constant from :py:mod:`kiva.constants`. Pick from ``NORMAL`` or
``BOLD``.
``weight`` is a weight constant from :py:mod:`kiva.constants`. Pick from
``WEIGHT_NORMAL`` or ``WEIGHT_BOLD``. Some backends support additional weights
``WEIGHT_THIN``, ``WEIGHT_EXTRALIGHT``, ``WEIGHT_LIGHT``, ``WEIGHT_MEDIUM``,
``WEIGHT_SEMIBOLD``, ``WEIGHT_BOLD``, ``WEIGHT_EXTRABOLD``, ``WEIGHT_HEAVY``,
``WEIGHT_EXTRAHEAVY``. Backends that only know about bold and normal weights
treat any weight of semi-bold or more as bold, and all others as normal weight.

``style`` is a constant from :py:mod:`kiva.constants`. Pick from ``NORMAL`` or
``ITALIC``.
Expand Down
4 changes: 3 additions & 1 deletion docs/source/kiva/quickref.rst
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,9 @@ draw_mode:
FILL, EOF_FILL, STROKE, FILL_STROKE, EOF_FILL_STROKE

text_style:
NORMAL, BOLD, ITALIC
NORMAL, ITALIC
text_weight:
WEIGHT_NORMAL, WEIGHT_BOLD (some backends support additional weights)
text_draw_mode:
TEXT_FILL, TEXT_STROKE, TEXT_FILL_STROKE, TEXT_INVISIBLE, TEXT_FILL_CLIP,
TEXT_STROKE_CLIP, TEXT_FILL_STROKE_CLIP, TEXT_CLIP
Expand Down
5 changes: 4 additions & 1 deletion kiva/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,10 @@
SCALE_CTM, TRANSLATE_CTM, ROTATE_CTM, CONCAT_CTM, LOAD_CTM,
NO_MARKER, SQUARE_MARKER, DIAMOND_MARKER, CIRCLE_MARKER,
CROSSED_CIRCLE_MARKER, CROSS_MARKER, TRIANGLE_MARKER,
INVERTED_TRIANGLE_MARKER, PLUS_MARKER, DOT_MARKER, PIXEL_MARKER
INVERTED_TRIANGLE_MARKER, PLUS_MARKER, DOT_MARKER, PIXEL_MARKER,
WEIGHT_THIN, WEIGHT_EXTRALIGHT, WEIGHT_LIGHT, WEIGHT_NORMAL, WEIGHT_MEDIUM,
WEIGHT_SEMIBOLD, WEIGHT_BOLD, WEIGHT_EXTRABOLD, WEIGHT_HEAVY,
WEIGHT_EXTRAHEAVY
)
from ._cython_speedups import points_in_polygon
from .fonttools import add_application_fonts, Font
Expand Down
11 changes: 10 additions & 1 deletion kiva/blend2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@
"rgba32": blend2d.Format.XRGB32,
}

# map used in select_font
font_styles = {
"regular": (constants.WEIGHT_NORMAL, constants.NORMAL),
"bold": (constants.WEIGHT_BOLD, constants.NORMAL),
"italic": (constants.WEIGHT_NORMAL, constants.ITALIC),
"bold italic": (constants.WEIGHT_BOLD, constants.ITALIC),
}

class GraphicsContext(object):
def __init__(self, size, *args, **kwargs):
Expand Down Expand Up @@ -467,13 +474,15 @@ def normalize_image(img):
def select_font(self, face_name, size=12, style="regular", encoding=None):
""" Set the font for the current graphics context.
"""
self.set_font(Font(face_name, size=size, style=style))
weight, style = font_styles[style.lower()]
self.set_font(Font(face_name, size=size, weight=weight, style=style))

def set_font(self, font):
""" Set the font for the current graphics context.
"""
spec = font.findfont()
self._kiva_font = font
# XXX doesn't handle .ttc/.otc files with multiple fonts
self.font = blend2d.Font(spec.filename, font.size)

def set_font_size(self, size):
Expand Down
11 changes: 4 additions & 7 deletions kiva/cairo.py
Original file line number Diff line number Diff line change
Expand Up @@ -966,25 +966,22 @@ def set_font(self, font):
A device-specific font object. In this case, a cairo FontFace
object. It's not clear how this can be used right now.
"""
if font.weight in (constants.BOLD, constants.BOLD_ITALIC):
# Cairo only handles bold/normal weights
if font.is_bold():
weight = cairo.FONT_WEIGHT_BOLD
else:
weight = cairo.FONT_WEIGHT_NORMAL

if font.style in (constants.ITALIC, constants.BOLD_ITALIC):
if font.style in contants.italic_styles:
style = cairo.FONT_SLANT_ITALIC
else:
style = cairo.FONT_SLANT_NORMAL

face_name = font.face_name
face_name = font.findfontname()

ctx = self._ctx
ctx.select_font_face(face_name, style, weight)
ctx.set_font_size(font.size)
# facename = font.face_name
# slant = font.style

# self._ctx.set_font_face(font)

def set_font_size(self, size):
""" Sets the size of the font.
Expand Down
32 changes: 24 additions & 8 deletions kiva/celiagg.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@
constants.FILL_STROKE: agg.DrawingMode.DrawFillStroke,
constants.EOF_FILL_STROKE: agg.DrawingMode.DrawEofFillStroke,
}
font_weights = {
constants.WEIGHT_THIN: agg.FontWeight.Thin,
constants.WEIGHT_EXTRALIGHT: agg.FontWeight.ExtraLight,
constants.WEIGHT_LIGHT: agg.FontWeight.Light,
constants.WEIGHT_NORMAL: agg.FontWeight.Regular,
constants.WEIGHT_MEDIUM: agg.FontWeight.Medium,
constants.WEIGHT_SEMIBOLD: agg.FontWeight.SemiBold,
constants.WEIGHT_BOLD: agg.FontWeight.Bold,
constants.WEIGHT_EXTRABOLD: agg.FontWeight.ExtraBold,
constants.WEIGHT_HEAVY: agg.FontWeight.Heavy,
constants.WEIGHT_EXTRAHEAVY: agg.FontWeight.Heavy,
}
text_modes = {
constants.TEXT_FILL: agg.TextDrawingMode.TextDrawRaster,
constants.TEXT_STROKE: agg.TextDrawingMode.TextDrawStroke,
Expand Down Expand Up @@ -81,6 +93,13 @@
['state', 'path', 'stroke', 'fill', 'transform', 'text_transform', 'font'],
)

# map used in select_font
font_styles = {
"regular": (constants.WEIGHT_NORMAL, constants.NORMAL),
"bold": (constants.WEIGHT_BOLD, constants.NORMAL),
"italic": (constants.WEIGHT_NORMAL, constants.ITALIC),
"bold italic": (constants.WEIGHT_BOLD, constants.ITALIC),
}

class GraphicsContext(object):
def __init__(self, size, *args, **kwargs):
Expand Down Expand Up @@ -630,22 +649,19 @@ def normalize_image(img):
def select_font(self, face_name, size=12, style='regular', encoding=None):
""" Set the font for the current graphics context.
"""
self.set_font(Font(face_name, size=size, style=style))
weight, style = font_styles[style.lower()]
self.set_font(Font(face_name, size=size, weight=weight, style=style))

def set_font(self, font):
""" Set the font for the current graphics context.
"""
if sys.platform in ('win32', 'cygwin'):
# Have to pass weight and italic on Win32
italic = (font.style in (constants.ITALIC, constants.BOLD_ITALIC))
weight = agg.FontWeight.Regular
if font.style in (constants.BOLD, constants.BOLD_ITALIC):
weight = agg.FontWeight.Bold
weight = font_weights.get(font._get_weight(), agg.FontWeight.Regular)
style = (font.style in constants.italic_styles)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bool should still be named italic, I think. There's no reason why style=True should mean "this font is italic".


# Win32 font selection is handled by the OS
self.font = agg.Font(
font.findfontname(), font.size, weight, italic
)
self.font = agg.Font(font.findfontname(), font.size, weight, style)
else:
# FreeType font selection is handled by kiva
spec = font.findfont()
Expand Down
16 changes: 16 additions & 0 deletions kiva/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@
ITALIC = 2
BOLD_ITALIC = 3

# convenience sets for styles
bold_styles = {BOLD, BOLD_ITALIC}
italic_styles = {ITALIC, BOLD_ITALIC}

# Font families, as defined by the Windows API, and their CSS equivalents
DEFAULT = 0
SWISS = 1 # Sans-serif
Expand All @@ -65,6 +69,18 @@
SCRIPT = 5 # Cursive
TELETYPE = 6

# Font weight constants
WEIGHT_THIN = 100
WEIGHT_EXTRALIGHT = 200
WEIGHT_LIGHT = 300
WEIGHT_NORMAL = 400
WEIGHT_MEDIUM = 500
WEIGHT_SEMIBOLD = 600
WEIGHT_BOLD = 700
WEIGHT_EXTRABOLD = 800
WEIGHT_HEAVY = 900
WEIGHT_EXTRAHEAVY = 1000

# -----------------------------------------------------------------------------
# Text Drawing Mode Constants
# -----------------------------------------------------------------------------
Expand Down
62 changes: 50 additions & 12 deletions kiva/fonttools/font.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
specification strings into Font instances.
"""
import copy
import warnings

from kiva.constants import (
BOLD_ITALIC, BOLD, DECORATIVE, DEFAULT, ITALIC, MODERN, NORMAL, ROMAN,
SCRIPT, SWISS, TELETYPE,
BOLD, DECORATIVE, DEFAULT, ITALIC, MODERN, NORMAL, ROMAN,
SCRIPT, SWISS, TELETYPE, WEIGHT_BOLD, WEIGHT_MEDIUM, WEIGHT_NORMAL,
bold_styles, italic_styles
)
from kiva.fonttools._query import FontQuery
from kiva.fonttools.font_manager import default_font_manager
Expand All @@ -29,7 +31,7 @@
"modern": MODERN,
}
font_styles = {"italic": ITALIC}
font_weights = {"bold": BOLD}
font_weights = {"bold": WEIGHT_BOLD}
font_noise = {"pt", "point", "family"}


Expand All @@ -42,7 +44,7 @@ def str_to_font(fontspec):
point_size = 10
family = DEFAULT
style = NORMAL
weight = NORMAL
weight = WEIGHT_NORMAL
underline = 0
facename = []
for word in fontspec.split():
Expand Down Expand Up @@ -93,8 +95,9 @@ class Font(object):
TELETYPE: "monospace",
}

def __init__(self, face_name="", size=12, family=SWISS, weight=NORMAL,
style=NORMAL, underline=0, encoding=DEFAULT):
def __init__(self, face_name="", size=12, family=SWISS,
weight=WEIGHT_NORMAL, style=NORMAL, underline=0,
encoding=DEFAULT):
if (not isinstance(face_name, str)
or not isinstance(size, int)
or not isinstance(family, int)
Expand All @@ -112,6 +115,10 @@ def __init__(self, face_name="", size=12, family=SWISS, weight=NORMAL,
self.underline = underline
self.encoding = encoding

# correct the style and weight if needed (can be removed in Enable 7)
self.weight = self._get_weight()
self.style = style & ~BOLD

def findfont(self, language=None):
""" Returns the file name and face index of the font that most closely
matches our font properties.
Expand Down Expand Up @@ -146,18 +153,26 @@ def findfontname(self, language=None):

return query.get_name()

def is_bold(self):
"""Is the font considered bold or not?

This is a convenience method for backends which don't fully support
font weights. We consider a font to be bold if its weight is more
than medium.
"""
weight = self._get_weight()
return (weight > WEIGHT_MEDIUM)

def _make_font_query(self):
""" Returns a FontQuery object that encapsulates our font properties.
"""
# XXX: change the weight to a numerical value
if self.style == BOLD or self.style == BOLD_ITALIC:
weight = "bold"
else:
weight = "normal"
if self.style == ITALIC or self.style == BOLD_ITALIC:
weight = self._get_weight()

if self.style in italic_styles:
style = "italic"
else:
style = "normal"

query = FontQuery(
family=self.familymap[self.family],
style=style,
Expand All @@ -168,6 +183,29 @@ def _make_font_query(self):
query.set_name(self.face_name)
return query

def _get_weight(self):
"""Get a corrected weight value from the font.

Note: this is a temporary method that will be removed in Enable 7.
"""
if self.weight == BOLD:
warnings.warn(
"Use WEIGHT_BOLD instead of BOLD for Font.weight",
DeprecationWarning
)
return WEIGHT_BOLD
elif self.style in bold_styles:
warnings.warn(
"Set Font.weight to WEIGHT_BOLD instead of Font.style to "
"BOLD or BOLD_STYLE",
DeprecationWarning
)
# if weight is default, and style is bold, report as bold
if self.weight == WEIGHT_NORMAL:
return WEIGHT_BOLD

return self.weight

def _get_name(self):
return self.face_name

Expand Down
Loading