# ---------------------------------------------------------------------------- # pyglet # Copyright (c) 2006-2008 Alex Holkner # 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. ''' __docformat__ = 'restructuredtext' __version__ = '$Id: base.py 2278 2008-09-23 12:18:50Z Alex.Holkner $' import unicodedata from pyglet.gl import * from pyglet import image _other_grapheme_extend = \ map(unichr, [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 = \ map(unichr, range(0xe40, 0xe45) + 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 map(unichr, [0x000d, 0x000a, 0x200c, 0x200d]) _extend = lambda c, cc: _grapheme_extend(c, cc) or \ c in map(unichr, [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. :since: pyglet 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. ''' advance = 0 vertices = (0, 0, 0, 0) def set_bearings(self, baseline, left_side_bearing, advance): '''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. ''' self.advance = advance self.vertices = ( left_side_bearing, -baseline, left_side_bearing + self.width, -baseline + self.height) 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]) 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 apply_blend_state(self): '''Set the OpenGL blend state for the glyphs in this texture. ''' glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) glEnable(GL_BLEND) 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 self.x + image.width > self.width: self.x = 0 self.y += self.line_height 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(object): '''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(object): '''Abstract font class able to produce glyphs. To construct a font, use `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 = 256 texture_height = 256 texture_internalformat = GL_ALPHA # 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 = {} @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 `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 for texture in self.textures: glyph = texture.fit(image) if glyph: break if not glyph: if image.width > self.texture_width or \ image.height > self.texture_height: texture = self.texture_class.create_for_size(GL_TEXTURE_2D, image.width * 2, image.height * 2, self.texture_internalformat) self.texture_width = texture.width self.texture_height = texture.height else: texture = self.texture_class.create_for_size(GL_TEXTURE_2D, self.texture_width, self.texture_height, self.texture_internalformat) self.textures.insert(0, texture) glyph = texture.fit(image) return glyph 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(unicode(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 enountered 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 and len(glyphs) > 0: 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