CampBuddy/Camp.Buddy v2.2.1/Camp_Buddy-2.2.1-pc/renpy/text/font.py
2025-03-03 23:00:33 +01:00

818 lines
23 KiB
Python

# Copyright 2004-2019 Tom Rothamel <pytom@bishoujo.us>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from __future__ import print_function
import pygame_sdl2 as pygame
try:
import xml.etree.ElementTree as etree
except:
pass
import renpy.display
import renpy.text.ftfont as ftfont
import renpy.text.textsupport as textsupport
ftfont.init() # @UndefinedVariable
WHITE = (255, 255, 255, 255)
BLACK = (0, 0, 0, 255)
def is_zerowidth(char):
if char == 0x200b: # Zero-width space.
return True
if char == 0x200c: # Zero-width non-joiner.
return True
if char == 0x200d: # Zero-width joiner.
return True
if char == 0x2060: # Word joiner.
return True
if char == 0xfeff: # Zero width non-breaking space.
return True
return False
class ImageFont(object):
# ImageFonts are expected to have the following fields defined by
# a subclass:
# Font global:
# height - The line height, the height of each character cell.
# kerns - The kern between each pair of characters.
# default_kern - The default kern.
# baseline - The y offset of the font baseline.
# Per-character:
# width - The width of each character.
# advance - The advance of each character.
# offsets - The x and y offsets of each character.
# chars - A map from a character to the surface containing that character.
def glyphs(self, s):
rv = [ ]
if not s:
return rv
for c in s:
g = textsupport.Glyph() # @UndefinedVariable
g.character = ord(c)
g.ascent = self.baseline
g.line_spacing = self.height
if not is_zerowidth(g.character):
width = self.width.get(c, None)
if width is None:
raise Exception("Character {0!r} not found in image-based font.".format(c))
g.width = self.width[c]
g.advance = self.advance[c]
else:
g.width = 0
g.advance = 0
rv.append(g)
# Compute kerning.
for i in range(len(s) - 1):
kern = self.kerns.get(s[i] + s[i+1], self.default_kern)
rv[i].advance += kern
return rv
def bounds(self, glyphs, bounds):
return bounds
def draw(self, target, xo, yo, color, glyphs, underline, strikethrough, black_color):
if black_color is None:
return
for g in glyphs:
if not g.width:
continue
c = unichr(g.character)
cxo, cyo = self.offsets[c]
x = g.x + xo + cxo
y = g.y + yo + cyo - g.ascent
char_surf = self.chars[c]
if renpy.config.recolor_sfonts:
if color != WHITE or black_color != BLACK:
new_surf = renpy.display.pgrender.surface(char_surf.get_size(), True)
renpy.display.module.twomap(char_surf, new_surf, color, black_color)
char_surf = new_surf
target.blit(char_surf, (x, y))
class SFont(ImageFont):
def __init__(self,
filename,
spacewidth,
default_kern,
kerns,
charset,
baseline=None):
super(SFont, self).__init__()
self.filename = filename
self.spacewidth = spacewidth
self.default_kern = default_kern
self.kerns = kerns
self.charset = charset
self.baseline = baseline
def load(self):
self.chars = { } # W0201
self.width = { } # W0201
self.advance = { } # W0201
self.offsets = { } # W0201
# Load in the image.
surf = renpy.display.im.Image(self.filename).load(unscaled=True)
sw, sh = surf.get_size()
height = sh
self.height = height # W0201
if self.baseline is None:
self.baseline = height # W0201
elif self.baseline < 0:
# Negative value is the distance from the bottom (vs top)
self.baseline = height + self.baseline # W0201
# Create space characters.
self.chars[u' '] = renpy.display.pgrender.surface((self.spacewidth, height), True)
self.width[u' '] = self.spacewidth
self.advance[u' '] = self.spacewidth
self.offsets[u' '] = (0, 0)
self.chars[u'\u200b'] = renpy.display.pgrender.surface((0, height), True)
self.width[u'\u200b'] = 0
self.advance[u'\u200b'] = 0
self.offsets[u'\u200b'] = (0, 0)
self.chars[u'\u00a0'] = self.chars[u' ']
self.width[u'\u00a0'] = self.width[u' ']
self.advance[u'\u00a0'] = self.advance[u' ']
self.offsets[u'\u00a0'] = self.offsets[u' ']
# The color key used to separate characters.
i = 0
while True:
key = surf.get_at((i, 0))
if key[3] != 0:
break
i += 1
ci = 0
# Find real characters, create them.
while i < sw and ci < len(self.charset):
if surf.get_at((i, 0)) != key:
start = i
i += 1
while i < sw:
if surf.get_at((i, 0)) == key:
break
i += 1
c = self.charset[ci]
ci += 1
ss = surf.subsurface((start, 0, i - start, height))
ss = renpy.display.scale.surface_scale(ss)
self.chars[c] = ss
self.width[c] = i - start
self.advance[c] = i - start
self.offsets[c] = (0, 0)
i += 1
class MudgeFont(ImageFont):
def __init__(self,
filename,
xml,
spacewidth,
default_kern,
kerns):
super(MudgeFont, self).__init__()
self.filename = filename
self.xml = xml
self.spacewidth = spacewidth
self.default_kern = default_kern
self.kerns = kerns
def load(self):
self.chars = { } # W0201
self.width = { } # W0201
self.advance = { } # W0201
self.offsets = { } # W0201
# Load in the image.
surf = renpy.display.im.Image(self.filename).load(unscaled=True)
# Parse the xml file.
tree = etree.fromstring(renpy.loader.load(self.xml).read())
height = 0
# Find each character.
for e in tree.findall("char"):
char = int(e.attrib["id"])
if char < 0:
continue
c = unichr(char)
x = int(e.attrib["x"])
y = int(e.attrib["y"])
w = int(e.attrib["width"])
h = int(e.attrib["height"])
ss = surf.subsurface((x, y, w, h))
ss = renpy.display.scale.surface_scale(ss)
self.chars[c] = ss
self.width[c] = w
self.advance[c] = w
self.offsets[c] = (0, 0)
height = max(height, h)
self.height = height # W0201
self.baseline = height # W0201
# Create space characters.
if u' ' not in self.chars:
self.chars[u' '] = renpy.display.pgrender.surface((self.spacewidth, height), True)
self.width[u' '] = self.spacewidth
self.advance[u' '] = self.spacewidth
self.offsets[u' '] = (0, 0)
if u'\u00a0' not in self.chars:
self.chars[u'\u00a0'] = self.chars[u' ']
self.width[u'\u00a0'] = self.width[u' ']
self.advance[u'\u00a0'] = self.advance[u' ']
self.offsets[u'\u00a0'] = self.offsets[u' ']
self.chars[u'\u200b'] = renpy.display.pgrender.surface((0, height), True)
self.width[u'\u200b'] = 0
self.advance[u'\u200b'] = 0
self.offsets[u'\u200b'] = (0, 0)
def parse_bmfont_line(l):
w = ""
line = [ ]
quote = False
for c in l:
if c == "\r" or c == "\n":
continue
if c == " " and not quote:
if w:
line.append(w)
w = ""
continue
if c == "\"":
quote = not quote
continue
w += c
if w:
line.append(w)
map = dict(i.split("=", 1) for i in line[1:]) # @ReservedAssignment
return line[0], map
class BMFont(ImageFont):
def __init__(self, filename):
super(BMFont, self).__init__()
self.filename = filename
def load(self):
self.chars = { } # W0201
self.width = { } # W0201
self.advance = { } # W0201
self.offsets = { } # W0201
self.kerns = { } # W0201
self.default_kern = 0 # W0201
pages = { }
f = renpy.loader.load(self.filename)
for l in f:
kind, args = parse_bmfont_line(l)
if kind == "common":
self.height = int(args["lineHeight"]) # W0201
self.baseline = int(args["base"]) # W0201
elif kind == "page":
pages[int(args["id"])] = renpy.display.im.Image(args["file"]).load(unscaled=True)
elif kind == "char":
c = unichr(int(args["id"]))
x = int(args["x"])
y = int(args["y"])
w = int(args["width"])
h = int(args["height"])
xo = int(args["xoffset"])
yo = int(args["yoffset"])
xadvance = int(args["xadvance"])
page = int(args["page"])
ss = pages[page].subsurface((x, y, w, h))
ss = renpy.display.scale.surface_scale(ss)
self.chars[c] = ss
self.width[c] = w + xo
self.advance[c] = xadvance
self.offsets[c] = (xo, yo)
elif kind == "kerning":
first = unichr(int(args["first"]))
second = unichr(int(args["second"]))
self.kerns[first + second] = int(args["amount"])
f.close()
if u'\u00a0' not in self.chars:
self.chars[u'\u00a0'] = self.chars[u' ']
self.width[u'\u00a0'] = self.width[u' ']
self.advance[u'\u00a0'] = self.advance[u' ']
self.offsets[u'\u00a0'] = self.offsets[u' ']
self.chars[u'\u200b'] = renpy.display.pgrender.surface((0, self.height), True)
self.width[u'\u200b'] = 0
self.advance[u'\u200b'] = 0
self.offsets[u'\u200b'] = (0, 0)
class ScaledImageFont(ImageFont):
"""
Represents an imagefont scaled by a given factor.
"""
def __init__(self, parent, factor):
def scale(n):
return int(round(n * factor))
self.height = scale(parent.height)
self.baseline = scale(parent.baseline)
self.default_kern = scale(parent.default_kern)
self.width = { k : scale(v) for k, v in parent.width.iteritems() }
self.advance = { k : scale(v) for k, v in parent.advance.iteritems() }
self.offsets = { k : (scale(v[0]), scale(v[1])) for k, v in parent.offsets.iteritems() }
self.kerns = { k : scale(v) for k, v in parent.kerns.iteritems() }
self.chars = { }
for k, v in parent.chars.iteritems():
w, h = v.get_size()
nw = scale(w)
nh = scale(h)
self.chars[k] = renpy.display.scale.smoothscale(v, (nw, nh))
def register_sfont(name=None, size=None, bold=False, italics=False, underline=False,
filename=None, spacewidth=10, baseline=None, default_kern=0, kerns={},
charset=u"!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"):
"""
:doc: image_fonts
This registers an SFont with the given details. Please note that size, bold,
italic, and underline are all advisory (used for matching), and do not
change the appearance of the font.
`More information about SFont. <http://www.linux-games.com/sfont/>`_
`name`
The name of the font being registered, a string.
`size`
The size of the font being registered, an integer.
`bold`
The boldness of the font being registered, a boolean.
`italics`
The italicness of the font being registered, a boolean.
`underline`
An ignored parameter.
`filename`
The file containing the sfont image, a string.
`spacewidth`
The width of a space character, an integer in pixels.
`baseline`
The distance from the top of the font to the baseline (the invisible
line letters sit on), an integer in pixels. If this font is mixed with
other fonts, their baselines will be aligned. Negative values indicate
distance from the bottom of the font instead, and ``None`` means the
baseline equals the height (i.e., is at the very bottom of the font).
`default_kern`
The default kern spacing between characters, in pixels.
`kerns`
A map from two-character strings to the kern that should be used between
those characters.
`charset`
The character set of the font. A string containing characters in
the order in which they are found in the image. The default character
set for a SFont is::
! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ?
@ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _
` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~
"""
if name is None or size is None or filename is None:
raise Exception("When registering an SFont, the font name, font size, and filename are required.")
sf = SFont(filename, spacewidth, default_kern, kerns, charset, baseline)
image_fonts[(name, size, bold, italics)] = sf
def register_mudgefont(name=None, size=None, bold=False, italics=False, underline=False,
filename=None, xml=None, spacewidth=10, default_kern=0, kerns={}):
"""
:doc: image_fonts
This registers a MudgeFont with the given details. Please note that size,
bold, italic, and underline are all advisory (used for matching), and do not
change the appearance of the font.
Please see the `MudgeFont home page <http://www.larryhastings.com/programming/mudgefont/>`_
for the tool that creates MudgeFonts. Ren'Py assumes that character codes
found in the MudgeFont xml file are unicode character numbers, and ignores
negative character codes.
`name`
The name of the font being registered, a string.
`size`
The size of the font being registered, an integer.
`bold`
The boldness of the font being registered, a boolean.
`italics`
The italicness of the font being registered, a boolean.
`underline`
An ignored parameter.
`filename`
The file containing the MudgeFont image, a string. The image is usually
a TGA file, but could be a PNG or other format that Ren'Py supports.
`xml`
The xml file containing information generated by the MudgeFont tool.
`spacewidth`
The width of a space character, an integer in pixels.
`default_kern`
The default kern spacing between characters, in pixels.
`kerns`
A map from two-character strings to the kern that should be used between
those characters.
"""
if name is None or size is None or filename is None or xml is None:
raise Exception("When registering a Mudge Font, the font name, font size, filename, and xml filename are required.")
mf = MudgeFont(filename, xml, spacewidth, default_kern, kerns)
image_fonts[(name, size, bold, italics)] = mf
def register_bmfont(name=None, size=None, bold=False, italics=False, underline=False,
filename=None):
"""
:doc: image_fonts
This registers a BMFont with the given details. Please note that size, bold,
italic, and underline are all advisory (used for matching), and do not
change the appearance of the font.
Please see the `BMFont home page <http://www.angelcode.com/products/bmfont/>`_
for the tool that creates BMFonts. Ren'Py expects that the filename
parameter will be to a file in the BMFont text format, that describes a
32-bit font. The Alpha channel should contain the font information, while
the Red, Green, and Blue channels should be set to one. The image files,
kerning, and other control information is read out of the BMFont file.
We recommend including Latin and General Punctuation as part of your BMFont,
to ensure all of the Ren'Py interface can render.
`name`
The name of the font being registered, a string.
`size`
The size of the font being registered, an integer.
`bold`
The boldness of the font being registered, a boolean.
`italics`
The italicness of the font being registered, a boolean.
`underline`
An ignored parameter.
`filename`
The file containing BMFont control information.
"""
bmf = BMFont(filename)
image_fonts[(name, size, bold, italics)] = bmf
# A map from face name to ftfont.FTFace
face_cache = { }
def load_face(fn):
if fn in face_cache:
return face_cache[fn]
orig_fn = fn
# Figure out the font index.
index = 0
if "@" in fn:
index, fn = fn.split("@", 1)
index = int(index)
font_file = None
try:
font_file = renpy.loader.load(fn)
except IOError:
if (not renpy.config.developer) or renpy.config.allow_sysfonts:
# Let's try to find the font on our own.
fonts = [ i.strip().lower() for i in fn.split(",") ]
pygame.sysfont.initsysfonts()
for v in pygame.sysfont.Sysfonts.itervalues():
if v is not None:
for _flags, ffn in v.iteritems():
for i in fonts:
if ffn.lower().endswith(i):
font_file = file(ffn, "rb")
break
if font_file:
break
if font_file:
break
if font_file is None:
raise Exception("Could not find font {0!r}.".format(orig_fn))
rv = ftfont.FTFace(font_file, index, orig_fn) # @UndefinedVariable
face_cache[orig_fn] = rv
return rv
# Caches of fonts.
image_fonts = { }
# A cache of scaled image fonts.
scaled_image_fonts = { }
# A cache of scaled faces.
font_cache = { }
# The last_scale we last accessed fonts at. (Used to clear caches.)
last_scale = 1.0
def get_font(fn, size, bold, italics, outline, antialias, vertical, hinting, scale):
# If the scale changed, invalidate caches of scaled fonts.
global last_scale
if (scale != 1.0) and (scale != last_scale):
scaled_image_fonts.clear()
font_cache.clear()
last_scale = scale
# Perform replacement.
t = (fn, bold, italics)
fn, bold, italics = renpy.config.font_replacement_map.get(t, t)
# Image fonts.
key = (fn, size, bold, italics)
rv = image_fonts.get(key, None)
if rv is not None:
if scale != 1.0:
if key in scaled_image_fonts:
rv = scaled_image_fonts[key]
else:
rv = ScaledImageFont(rv, scale)
scaled_image_fonts[key] = rv
return rv
# Check for a cached TTF.
key = (fn, size, bold, italics, outline, antialias, vertical, hinting, scale)
rv = font_cache.get(key, None)
if rv is not None:
return rv
# Load a TTF.
face = load_face(fn)
rv = ftfont.FTFont(face, int(size * scale), bold, italics, outline, antialias, vertical, hinting) # @UndefinedVariable
font_cache[key] = rv
return rv
def free_memory():
"""
Clears the font cache.
"""
scaled_image_fonts.clear()
font_cache.clear()
def load_fonts():
for i in image_fonts.itervalues():
i.load()
for i in renpy.config.preload_fonts:
load_face(i)
class FontGroup(object):
"""
:doc: font_group
:args: ()
A group of fonts that can be used as a single font.
"""
def __init__(self):
# A map from character index to font name. None is used for
# the default font.
self.map = { }
def add(self, font, start, end):
"""
:doc: font_group
Associates a range of characters with a `font`.
`start`
The start of the range. This may be a single-character string, or
an integer giving a unicode code point. If start is None, then the
font is used as the default.
`end`
The end of the range. This may be a single-character string, or an
integer giving a unicode code point.
When multiple .add() calls include the same character, the first call
takes precedence.
This returns the FontGroup, so that multiple calls to .add() can be
chained together.
"""
if start is None:
if isinstance(font, FontGroup):
for k, v in font.map.items():
if k not in self.map:
self.map[k] = v
else:
if None not in self.map:
self.map[None] = font
return self
if not isinstance(start, int):
start = ord(start)
if not isinstance(end, int):
end = ord(end)
if end < start:
raise Exception("In FontGroup.add, the start of a character range must be before the end of the range.")
for i in range(start, end+1):
if i not in self.map:
self.map[i] = font
return self
def segment(self, s):
"""
Segments `s` into fonts. Generates (font, string) tuples.
"""
mark = 0
pos = 0
old_font = None
for c in s:
n = ord(c)
font = self.map.get(ord(c), None)
if font is None:
font = self.map.get(None, None)
if font is None:
raise Exception("Character U+{0:04x} not found in FontGroup".format(n))
if font != old_font:
if pos:
yield old_font, s[mark:pos]
old_font = font
mark = pos
pos += 1
yield font, s[mark:]