Difficult-Rocket/libs/pyglet/font/base.py

455 lines
15 KiB
Python
Raw Normal View History

2021-04-16 23:21:06 +08:00
# ----------------------------------------------------------------------------
# pyglet
# Copyright (c) 2006-2008 Alex Holkner
2021-04-17 01:14:38 +08:00
# Copyright (c) 2008-2021 pyglet contributors
2021-04-16 23:21:06 +08:00
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
# * Neither the name of pyglet nor the names of its
# contributors may be used to endorse or promote products
# derived from this software without specific prior written
# permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ----------------------------------------------------------------------------
"""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):
2021-04-16 23:21:06 +08:00
# 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):
2021-04-16 23:21:06 +08:00
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.
"""
2021-04-17 01:14:38 +08:00
baseline = 0
lsb = 0
2021-04-16 23:21:06 +08:00
advance = 0
vertices = (0, 0, 0, 0)
2021-04-17 01:14:38 +08:00
def set_bearings(self, baseline, left_side_bearing, advance, x_offset=0, y_offset=0):
2021-04-16 23:21:06 +08:00
"""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.
2021-04-17 01:14:38 +08:00
`offset_x` : int
2021-12-26 23:06:03 +08:00
Distance to move the glyph horizontally from its default position.
2021-04-17 01:14:38 +08:00
`offset_y` : int
2021-12-26 23:06:03 +08:00
Distance to move the glyph vertically from its default position.
2021-04-16 23:21:06 +08:00
"""
2021-04-17 01:14:38 +08:00
self.baseline = baseline
self.lsb = left_side_bearing
2021-04-16 23:21:06 +08:00
self.advance = advance
2021-04-17 01:14:38 +08:00
2021-04-16 23:21:06 +08:00
self.vertices = (
2021-04-17 01:14:38 +08:00
left_side_bearing + x_offset,
-baseline + y_offset,
left_side_bearing + self.width + x_offset,
-baseline + self.height + y_offset)
2021-04-16 23:21:06 +08:00
# def draw(self):
# """Debug method.
#
# Use the higher level APIs for performance and kerning.
# """
# glBindTexture(GL_TEXTURE_2D, self.owner.id)
# glBegin(GL_QUADS)
# self.draw_quad_vertices()
# glEnd()
#
# def draw_quad_vertices(self):
# """Debug method.
#
# Use the higher level APIs for performance and kerning.
# """
# glTexCoord3f(*self.tex_coords[:3])
# glVertex2f(self.vertices[0], self.vertices[1])
# glTexCoord3f(*self.tex_coords[3:6])
# glVertex2f(self.vertices[2], self.vertices[1])
# glTexCoord3f(*self.tex_coords[6:9])
# glVertex2f(self.vertices[2], self.vertices[3])
# glTexCoord3f(*self.tex_coords[9:12])
# glVertex2f(self.vertices[0], self.vertices[3])
2021-04-16 23:21:06 +08:00
def get_kerning_pair(self, right_glyph):
"""Not implemented.
"""
return 0
class GlyphTextureAtlas(image.Texture):
"""A texture within which glyphs can be drawn.
"""
region_class = Glyph
x = 0
y = 0
line_height = 0
def fit(self, image):
"""Place `image` within this texture.
:Parameters:
`image` : `pyglet.image.AbstractImage`
Image to place within the texture.
:rtype: `Glyph`
:return: The glyph representing the image from this texture, or None
if the image doesn't fit.
"""
if image.width > self.width or image.height > self.height:
return None
if self.x + image.width > self.width:
self.x = 0
self.y += self.line_height + 1
self.line_height = 0
if self.y + image.height > self.height:
return None
self.line_height = max(self.line_height, image.height)
region = self.get_region(
self.x, self.y, image.width, image.height)
if image.width > 0:
region.blit_into(image, 0, 0, 0)
self.x += image.width + 1
return region
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
# TODO: rewrite text.layout._default_shader_program to use GL_R8 or GL_RED
texture_internalformat = GL_RGBA
2021-04-16 23:21:06 +08:00
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 = GlyphTextureAtlas
def __init__(self):
self.textures = []
self.glyphs = {}
2021-08-13 12:25:29 +08:00
@property
def name(self):
"""Return the Family Name of the font as a string."""
raise NotImplementedError
2021-04-16 23:21:06 +08:00
@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):
"""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.
:rtype: `Glyph`
"""
glyph = None
self._adapt_texture_size(image)
for texture in self.textures:
glyph = texture.fit(image)
if glyph:
break
if not glyph:
texture = self.texture_class.create(self.texture_width,
self.texture_height,
GL_TEXTURE_2D,
self.texture_internalformat,
self.texture_min_filter,
self.texture_mag_filter)
2021-04-16 23:21:06 +08:00
self.textures.insert(0, texture)
glyph = texture.fit(image)
return glyph
def _adapt_texture_size(self, image):
if image.width > self.texture_width or image.height > self.texture_height:
largest_dimension = max(image.width, image.height)
self.texture_height = self.texture_width = largest_dimension * 4
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):
2021-04-16 23:21:06 +08:00
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