416 lines
13 KiB
Python
416 lines
13 KiB
Python
"""Abstract classes used by pyglet.font implementations.
|
|
|
|
These classes should not be constructed directly. Instead, use the functions
|
|
in `pyglet.font` to obtain platform-specific instances. You can use these
|
|
classes as a documented interface to the concrete classes.
|
|
"""
|
|
|
|
import unicodedata
|
|
|
|
from pyglet.gl import *
|
|
from pyglet import image
|
|
|
|
_other_grapheme_extend = list(map(chr, [0x09be, 0x09d7, 0x0be3, 0x0b57, 0x0bbe, 0x0bd7, 0x0cc2,
|
|
0x0cd5, 0x0cd6, 0x0d3e, 0x0d57, 0x0dcf, 0x0ddf, 0x200c,
|
|
0x200d, 0xff9e, 0xff9f])) # skip codepoints above U+10000
|
|
_logical_order_exception = list(map(chr, list(range(0xe40, 0xe45)) + list(range(0xec0, 0xec4))))
|
|
|
|
_grapheme_extend = lambda c, cc: cc in ('Me', 'Mn') or c in _other_grapheme_extend
|
|
|
|
_CR = u'\u000d'
|
|
_LF = u'\u000a'
|
|
_control = lambda c, cc: cc in ('ZI', 'Zp', 'Cc', 'Cf') and not \
|
|
c in list(map(chr, [0x000d, 0x000a, 0x200c, 0x200d]))
|
|
_extend = lambda c, cc: _grapheme_extend(c, cc) or \
|
|
c in list(map(chr, [0xe30, 0xe32, 0xe33, 0xe45, 0xeb0, 0xeb2, 0xeb3]))
|
|
_prepend = lambda c, cc: c in _logical_order_exception
|
|
_spacing_mark = lambda c, cc: cc == 'Mc' and c not in _other_grapheme_extend
|
|
|
|
|
|
def grapheme_break(left, right):
|
|
# GB1
|
|
if left is None:
|
|
return True
|
|
|
|
# GB2 not required, see end of get_grapheme_clusters
|
|
|
|
# GB3
|
|
if left == _CR and right == _LF:
|
|
return False
|
|
|
|
left_cc = unicodedata.category(left)
|
|
|
|
# GB4
|
|
if _control(left, left_cc):
|
|
return True
|
|
|
|
right_cc = unicodedata.category(right)
|
|
|
|
# GB5
|
|
if _control(right, right_cc):
|
|
return True
|
|
|
|
# GB6, GB7, GB8 not implemented
|
|
|
|
# GB9
|
|
if _extend(right, right_cc):
|
|
return False
|
|
|
|
# GB9a
|
|
if _spacing_mark(right, right_cc):
|
|
return False
|
|
|
|
# GB9b
|
|
if _prepend(left, left_cc):
|
|
return False
|
|
|
|
# GB10
|
|
return True
|
|
|
|
|
|
def get_grapheme_clusters(text):
|
|
"""Implements Table 2 of UAX #29: Grapheme Cluster Boundaries.
|
|
|
|
Does not currently implement Hangul syllable rules.
|
|
|
|
:Parameters:
|
|
`text` : unicode
|
|
String to cluster.
|
|
|
|
.. versionadded:: 1.1.2
|
|
|
|
:rtype: List of `unicode`
|
|
:return: List of Unicode grapheme clusters
|
|
"""
|
|
clusters = []
|
|
cluster = ''
|
|
left = None
|
|
for right in text:
|
|
if cluster and grapheme_break(left, right):
|
|
clusters.append(cluster)
|
|
cluster = ''
|
|
elif cluster:
|
|
# Add a zero-width space to keep len(clusters) == len(text)
|
|
clusters.append(u'\u200b')
|
|
cluster += right
|
|
left = right
|
|
|
|
# GB2
|
|
if cluster:
|
|
clusters.append(cluster)
|
|
return clusters
|
|
|
|
|
|
class Glyph(image.TextureRegion):
|
|
"""A single glyph located within a larger texture.
|
|
|
|
Glyphs are drawn most efficiently using the higher level APIs, for example
|
|
`GlyphString`.
|
|
|
|
:Ivariables:
|
|
`advance` : int
|
|
The horizontal advance of this glyph, in pixels.
|
|
`vertices` : (int, int, int, int)
|
|
The vertices of this glyph, with (0,0) originating at the
|
|
left-side bearing at the baseline.
|
|
`colored` : bool
|
|
If a glyph is colored by the font renderer, such as an emoji, it may
|
|
be treated differently by pyglet. For example, being omitted from text color shaders.
|
|
|
|
"""
|
|
baseline = 0
|
|
lsb = 0
|
|
advance = 0
|
|
vertices = (0, 0, 0, 0)
|
|
colored = False
|
|
|
|
def set_bearings(self, baseline, left_side_bearing, advance, x_offset=0, y_offset=0):
|
|
"""Set metrics for this glyph.
|
|
|
|
:Parameters:
|
|
`baseline` : int
|
|
Distance from the bottom of the glyph to its baseline;
|
|
typically negative.
|
|
`left_side_bearing` : int
|
|
Distance to add to the left edge of the glyph.
|
|
`advance` : int
|
|
Distance to move the horizontal advance to the next glyph.
|
|
`offset_x` : int
|
|
Distance to move the glyph horizontally from its default position.
|
|
`offset_y` : int
|
|
Distance to move the glyph vertically from its default position.
|
|
"""
|
|
self.baseline = baseline
|
|
self.lsb = left_side_bearing
|
|
self.advance = advance
|
|
|
|
self.vertices = (
|
|
left_side_bearing + x_offset,
|
|
-baseline + y_offset,
|
|
left_side_bearing + self.width + x_offset,
|
|
-baseline + self.height + y_offset)
|
|
|
|
def get_kerning_pair(self, right_glyph):
|
|
"""Not implemented.
|
|
"""
|
|
return 0
|
|
|
|
|
|
class GlyphTexture(image.Texture):
|
|
region_class = Glyph
|
|
|
|
|
|
class GlyphTextureAtlas(image.atlas.TextureAtlas):
|
|
"""A texture atlas containing glyphs."""
|
|
texture_class = GlyphTexture
|
|
|
|
def __init__(self, width=2048, height=2048, fmt=GL_RGBA, min_filter=GL_LINEAR, mag_filter=GL_LINEAR):
|
|
self.texture = self.texture_class.create(width, height, GL_TEXTURE_2D, fmt, min_filter, mag_filter, fmt=fmt)
|
|
self.allocator = image.atlas.Allocator(width, height)
|
|
|
|
|
|
class GlyphTextureBin(image.atlas.TextureBin):
|
|
"""Same as a TextureBin but allows you to specify filter of Glyphs."""
|
|
|
|
def add(self, img, fmt=GL_RGBA, min_filter=GL_LINEAR, mag_filter=GL_LINEAR, border=0):
|
|
for atlas in list(self.atlases):
|
|
try:
|
|
return atlas.add(img, border)
|
|
except image.atlas.AllocatorException:
|
|
# Remove atlases that are no longer useful (so that their textures
|
|
# can later be freed if the images inside them get collected).
|
|
if img.width < 64 and img.height < 64:
|
|
self.atlases.remove(atlas)
|
|
|
|
atlas = GlyphTextureAtlas(self.texture_width, self.texture_height, fmt, min_filter, mag_filter)
|
|
self.atlases.append(atlas)
|
|
return atlas.add(img, border)
|
|
|
|
|
|
class GlyphRenderer:
|
|
"""Abstract class for creating glyph images.
|
|
"""
|
|
|
|
def __init__(self, font):
|
|
pass
|
|
|
|
def render(self, text):
|
|
raise NotImplementedError('Subclass must override')
|
|
|
|
|
|
class FontException(Exception):
|
|
"""Generic exception related to errors from the font module. Typically
|
|
these relate to invalid font data."""
|
|
pass
|
|
|
|
|
|
class Font:
|
|
"""Abstract font class able to produce glyphs.
|
|
|
|
To construct a font, use :py:func:`pyglet.font.load`, which will instantiate the
|
|
platform-specific font class.
|
|
|
|
Internally, this class is used by the platform classes to manage the set
|
|
of textures into which glyphs are written.
|
|
|
|
:Ivariables:
|
|
`ascent` : int
|
|
Maximum ascent above the baseline, in pixels.
|
|
`descent` : int
|
|
Maximum descent below the baseline, in pixels. Usually negative.
|
|
"""
|
|
texture_width = 512
|
|
texture_height = 512
|
|
|
|
optimize_fit = True
|
|
glyph_fit = 100
|
|
|
|
texture_internalformat = GL_RGBA
|
|
texture_min_filter = GL_LINEAR
|
|
texture_mag_filter = GL_LINEAR
|
|
|
|
# These should also be set by subclass when known
|
|
ascent = 0
|
|
descent = 0
|
|
|
|
glyph_renderer_class = GlyphRenderer
|
|
texture_class = GlyphTextureBin
|
|
|
|
def __init__(self):
|
|
self.texture_bin = None
|
|
self.glyphs = {}
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the Family Name of the font as a string."""
|
|
raise NotImplementedError
|
|
|
|
@classmethod
|
|
def add_font_data(cls, data):
|
|
"""Add font data to the font loader.
|
|
|
|
This is a class method and affects all fonts loaded. Data must be
|
|
some byte string of data, for example, the contents of a TrueType font
|
|
file. Subclasses can override this method to add the font data into
|
|
the font registry.
|
|
|
|
There is no way to instantiate a font given the data directly, you
|
|
must use :py:func:`pyglet.font.load` specifying the font name.
|
|
"""
|
|
pass
|
|
|
|
@classmethod
|
|
def have_font(cls, name):
|
|
"""Determine if a font with the given name is installed.
|
|
|
|
:Parameters:
|
|
`name` : str
|
|
Name of a font to search for
|
|
|
|
:rtype: bool
|
|
"""
|
|
return True
|
|
|
|
def create_glyph(self, image, fmt=None):
|
|
"""Create a glyph using the given image.
|
|
|
|
This is used internally by `Font` subclasses to add glyph data
|
|
to the font. Glyphs are packed within large textures maintained by
|
|
`Font`. This method inserts the image into a font texture and returns
|
|
a glyph reference; it is up to the subclass to add metadata to the
|
|
glyph.
|
|
|
|
Applications should not use this method directly.
|
|
|
|
:Parameters:
|
|
`image` : `pyglet.image.AbstractImage`
|
|
The image to write to the font texture.
|
|
`fmt` : `int`
|
|
Override for the format and internalformat of the atlas texture
|
|
|
|
:rtype: `Glyph`
|
|
"""
|
|
if self.texture_bin is None:
|
|
if self.optimize_fit:
|
|
self.texture_width, self.texture_height = self._get_optimal_atlas_size(image)
|
|
self.texture_bin = GlyphTextureBin(self.texture_width, self.texture_height)
|
|
|
|
glyph = self.texture_bin.add(
|
|
image, fmt or self.texture_internalformat, self.texture_min_filter, self.texture_mag_filter, border=1)
|
|
|
|
return glyph
|
|
|
|
def _get_optimal_atlas_size(self, image_data):
|
|
"""Return the smallest size of atlas that can fit around 100 glyphs based on the image_data provided."""
|
|
# A texture glyph sheet should be able to handle all standard keyboard characters in one sheet.
|
|
# 26 Alpha upper, 26 lower, 10 numbers, 33 symbols, space = around 96 characters. (Glyph Fit)
|
|
aw, ah = self.texture_width, self.texture_height
|
|
|
|
atlas_size = None
|
|
|
|
# Just a fast check to get the smallest atlas size possible to fit.
|
|
i = 0
|
|
while not atlas_size:
|
|
fit = ((aw - (image_data.width + 2)) // (image_data.width + 2) + 1) * (
|
|
(ah - (image_data.height + 2)) // (image_data.height + 2) + 1)
|
|
|
|
if fit >= self.glyph_fit:
|
|
atlas_size = (aw, ah)
|
|
|
|
if i % 2:
|
|
aw *= 2
|
|
else:
|
|
ah *= 2
|
|
|
|
i += 1
|
|
|
|
return atlas_size
|
|
|
|
def get_glyphs(self, text):
|
|
"""Create and return a list of Glyphs for `text`.
|
|
|
|
If any characters do not have a known glyph representation in this
|
|
font, a substitution will be made.
|
|
|
|
:Parameters:
|
|
`text` : str or unicode
|
|
Text to render.
|
|
|
|
:rtype: list of `Glyph`
|
|
"""
|
|
glyph_renderer = None
|
|
glyphs = [] # glyphs that are committed.
|
|
for c in get_grapheme_clusters(str(text)):
|
|
# Get the glyph for 'c'. Hide tabs (Windows and Linux render
|
|
# boxes)
|
|
if c == '\t':
|
|
c = ' '
|
|
if c not in self.glyphs:
|
|
if not glyph_renderer:
|
|
glyph_renderer = self.glyph_renderer_class(self)
|
|
self.glyphs[c] = glyph_renderer.render(c)
|
|
glyphs.append(self.glyphs[c])
|
|
return glyphs
|
|
|
|
def get_glyphs_for_width(self, text, width):
|
|
"""Return a list of glyphs for `text` that fit within the given width.
|
|
|
|
If the entire text is larger than 'width', as much as possible will be
|
|
used while breaking after a space or zero-width space character. If a
|
|
newline is encountered in text, only text up to that newline will be
|
|
used. If no break opportunities (newlines or spaces) occur within
|
|
`width`, the text up to the first break opportunity will be used (this
|
|
will exceed `width`). If there are no break opportunities, the entire
|
|
text will be used.
|
|
|
|
You can assume that each character of the text is represented by
|
|
exactly one glyph; so the amount of text "used up" can be determined
|
|
by examining the length of the returned glyph list.
|
|
|
|
:Parameters:
|
|
`text` : str or unicode
|
|
Text to render.
|
|
`width` : int
|
|
Maximum width of returned glyphs.
|
|
|
|
:rtype: list of `Glyph`
|
|
|
|
:see: `GlyphString`
|
|
"""
|
|
glyph_renderer = None
|
|
glyph_buffer = [] # next glyphs to be added, as soon as a BP is found
|
|
glyphs = [] # glyphs that are committed.
|
|
for c in text:
|
|
if c == '\n':
|
|
glyphs += glyph_buffer
|
|
break
|
|
|
|
# Get the glyph for 'c'
|
|
if c not in self.glyphs:
|
|
if not glyph_renderer:
|
|
glyph_renderer = self.glyph_renderer_class(self)
|
|
self.glyphs[c] = glyph_renderer.render(c)
|
|
glyph = self.glyphs[c]
|
|
|
|
# Add to holding buffer and measure
|
|
glyph_buffer.append(glyph)
|
|
width -= glyph.advance
|
|
|
|
# If over width and have some committed glyphs, finish.
|
|
if width <= 0 < len(glyphs):
|
|
break
|
|
|
|
# If a valid breakpoint, commit holding buffer
|
|
if c in u'\u0020\u200b':
|
|
glyphs += glyph_buffer
|
|
glyph_buffer = []
|
|
|
|
# If nothing was committed, commit everything (no breakpoints found).
|
|
if len(glyphs) == 0:
|
|
glyphs = glyph_buffer
|
|
|
|
return glyphs
|
|
|
|
def __repr__(self):
|
|
return f"{self.__class__.__name__}('{self.name}')"
|