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

979 lines
21 KiB
Cython

# 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
include "linebreak.pxi"
cdef class Glyph:
def __cinit__(self):
self.delta_x_offset = 0
def __repr__(self):
return "<Glyph {0!r} time={1}>".format(self.character, self.time)
cdef class Line:
def __init__(self, int y, int height, list glyphs):
self.y = y
self.height = height
self.glyphs = glyphs
self.eop = False
def __repr__(self):
return "<Line y={0}, height={1}>".format(self.y, self.height)
# The maximum width of text we lay out. This should be quite a bit smaller
# than the maximum SDL surface width. (16384)
cdef int MAX_WIDTH
MAX_WIDTH = 8192
TEXT=1
TAG=2
PARAGRAPH=3
DISPLAYABLE=4
def tokenize(unicode s):
"""
This tokenizes a unicode string into text tags and tokens. It returns a list
of pairs, where each pair begins with TEXT, TAG or PARAGRAPH, and then has
the contents of the text run or tag.
"""
cdef int TEXT_STATE = 1
cdef int LEFT_BRACE_STATE = 2
cdef int TAG_STATE = 3
cdef int state = TEXT_STATE
cdef Py_UNICODE c
cdef unicode buf = u''
cdef list rv = [ ]
for c in s:
if state == TEXT_STATE:
if c == u'\n':
if buf:
rv.append((TEXT, buf))
rv.append((PARAGRAPH, u''))
buf = u''
continue
elif c == u'{':
state = LEFT_BRACE_STATE
continue
else:
buf += c
continue
elif state == LEFT_BRACE_STATE:
if c == u'{':
buf += c
state = TEXT_STATE
continue
elif c == u'}':
raise Exception("Empty text tag in {0!r}.".format(s))
else:
if buf:
rv.append((TEXT, buf))
buf = c
state = TAG_STATE
continue
elif state == TAG_STATE:
if c == u'}':
rv.append((TAG, buf))
buf = u''
state = TEXT_STATE
continue
else:
buf += c
if state != TEXT_STATE:
raise Exception("Open text tag at end of string {0!r}.".format(s))
if buf:
rv.append((TEXT, buf))
return rv
def annotate_western(list glyphs):
"""
Annotate the characters with line splitting information.
"""
cdef Glyph g
for g in glyphs:
# Don't split ruby.
if g.ruby != RUBY_NONE:
continue
if g.character == 0:
g.split = SPLIT_BEFORE
elif g.character == 0x20 or g.character == 0x200b:
g.split = SPLIT_INSTEAD
else:
g.split = SPLIT_NONE
# This is used to tailor the unicode break algorithm. If a character in this
# array is mapped to not
cdef char break_tailor[65536]
for i in range(0, 65536):
break_tailor[i] = BC_XX
def language_tailor(chars, cls):
"""
:doc: other
:args: (chars, cls)
This can be used to override the line breaking class of a character. For
example, the linebreaking class of a character can be set to ID to
treat it as an ideograph, which allows breaks before and after that
character.
`chars`
A string containing each of the characters to tailor.
`cls`
A string giving a character class. This should be one of the classes defined in Table
1 of `UAX #14: Unicode Line Breaking Algorithm <http://www.unicode.org/reports/tr14/tr14-30.html>`_.
"""
ncls = CLASSES.get(cls, BC_XX)
for c in chars:
break_tailor[ord(c)] = ncls
# cjk
#
# 0 = western
# 1 = loose
# 2 = normal
# 3 = strict
def annotate_unicode(list glyphs, bint no_ideographs, int cjk):
"""
Annotate unicode characters with information as to if they can be used
for linebreaking.
"""
cdef char old_type, new_type, tailor_type
cdef int space_pos, pos
cdef int len_glyphs = len(glyphs)
cdef int c
cdef char bc
cdef Glyph g, g1, old_g
cdef char *break_classes
old_type = BC_WJ
pos = 1
space_pos = 0
if not glyphs:
return
if cjk == 0:
break_classes = break_western
elif cjk == 1:
break_classes = break_cjk_loose
elif cjk == 2:
break_classes = break_cjk_normal
elif cjk == 3:
break_classes = break_cjk_strict
else:
break_classes = break_western
for pos from 1 <= pos < len_glyphs:
g = glyphs[pos]
c = g.character
if 0x20000 <= c <= 0x2ffff: # Supplemental Ideographic Plane
new_type = BC_ID
tailor_type = BC_XX
elif c > 65535: # Other non-basic planes.
new_type = BC_AL
tailor_type = BC_XX
else: # Basic plane - use lookup table.
new_type = break_classes[c]
tailor_type = break_tailor[c]
# If given no-ideographs, then turn ideographs and hangul syllables
# into alphabetic characters.
if no_ideographs and (
new_type == BC_H2 or
new_type == BC_H3 or
new_type == BC_ID or
new_type == BC_JL or
new_type == BC_JV or
new_type == BC_JT):
new_type = BC_AL
# Normalize the class by turning various groups into AL.
if (new_type >= BC_PITCH and new_type != BC_SP and new_type != BC_CB):
new_type = BC_AL
if tailor_type != BC_XX:
new_type = tailor_type
# If we have a space, record it and continue.
if new_type == BC_SP:
g.split = SPLIT_NONE
space_pos = pos
continue
# If we have a combining mark, continue.
if new_type == BC_CM:
g.split = SPLIT_NONE
continue
if new_type == BC_CB:
if old_type == BC_WJ or old_type == BC_GL:
g.split = SPLIT_NONE
else:
g.split = SPLIT_BEFORE
continue
if old_type == BC_CB:
if new_type == BC_WJ or new_type == BC_GL:
g.split = SPLIT_NONE
else:
g.split = SPLIT_BEFORE
continue
# Figure out the type of break opportunity we have here.
# ^ Prohibited break.
# % Indirect break.
# @ Prohibited break (combining mark)
# # Indirect break (combining mark)
# _ Direct break.
bc = break_rules[ old_type * BC_PITCH + new_type]
if bc == "%": # Indirect break.
if space_pos:
g1 = glyphs[space_pos]
g1.split = SPLIT_INSTEAD
g.split = SPLIT_NONE
elif bc == "_": # Direct break.
if space_pos:
g1 = glyphs[space_pos]
g1.split = SPLIT_INSTEAD
g.split = SPLIT_NONE
else:
g.split = SPLIT_BEFORE
else:
g.split = SPLIT_NONE
old_type = new_type
space_pos = 0
# Deal with ruby, by marking it as non-spacing.
old_g = glyphs[0]
old_g.split = SPLIT_NONE
for g in glyphs:
if g.character == 0:
g.split = SPLIT_BEFORE
if g.ruby == RUBY_TOP or g.ruby == RUBY_ALT:
g.split = SPLIT_NONE
elif g.ruby == RUBY_BOTTOM and old_g.ruby == RUBY_BOTTOM:
g.split = SPLIT_NONE
old_g = g
def linebreak_greedy(list glyphs, int first_width, int rest_width):
"""
Starting with a list of glyphs, decides where to split it. The result of
this is the same list of glyphs, but with some of the .split fields set
back to SPLIT_NONE, where we decided not to go ahead with the split after
all.
This is the greedy algorithm, which splits when the line is longer
than a specified width.
"""
cdef Glyph g, split_g
cdef float width, x, splitx, gwidth
width = first_width
split_g = None
# The x position of the current character. Invariant: x can never be more
# that one character-width greater than width.
x = 0
# The x position after splitting the line.
splitx = 0
for g in glyphs:
if g.ruby == RUBY_TOP:
continue
if g.ruby == RUBY_ALT:
continue
# If the x coordinate is greater than the width of the screen,
# split at the last split point, if any.
if x > width and split_g is not None:
x = splitx
split_g = None
width = rest_width
x += g.advance
splitx += g.advance
if g.split == SPLIT_INSTEAD:
if split_g is not None:
split_g.split = SPLIT_NONE
split_g = g
splitx = 0
elif g.split == SPLIT_BEFORE:
if split_g is not None:
split_g.split = SPLIT_NONE
split_g = g
splitx = g.advance
# Split at the last character, if necessary.
if x > width:
split_g = None
if split_g is not None:
split_g.split = SPLIT_NONE
def linebreak_nobreak(list glyphs):
"""
Linebreak without linebreaking.
"""
cdef Glyph g
for g in glyphs:
g.split = SPLIT_NONE
def linebreak_debug(list glyphs):
"""
Return a string giving the results of linebreaking a list of glyphs.
"""
cdef Glyph g
rv = ""
for g in glyphs:
if g.split == SPLIT_INSTEAD:
rv += "|"
elif g.split == SPLIT_BEFORE:
rv += "[" + unichr(g.character)
else:
rv += unichr(g.character)
return rv
def linebreak_list(list glyphs):
"""
Returns a list of unicode strings, one per broken line.
"""
cdef Glyph g
rv = [ ]
line = u""
for g in glyphs:
if g.split == SPLIT_INSTEAD:
rv.append(line)
line = u""
elif g.split == SPLIT_BEFORE:
rv.append(line)
line = unichr(g.character)
else:
line += unichr(g.character)
if line:
rv.append(line)
return rv
def place_horizontal(list glyphs, float start_x, float first_indent, float rest_indent):
"""
Place the glyphs horizontally, without taking into account the indentation
at the start of the line. Returns the width of the laid-out line.
"""
if not glyphs:
return 0
cdef Glyph g, old_g
cdef float x, maxx
x = start_x + first_indent
maxx = 0
old_g = None
for g in glyphs:
if g.ruby == RUBY_TOP:
continue
if g.ruby == RUBY_ALT:
continue
if g.split != SPLIT_NONE and old_g:
# When a glyph is at the end of the line, set its advance to
# be its width. (This makes things like strikeout and underline
# easier, since we only need consider advance.)
old_g.advance = old_g.width
if g.split == SPLIT_INSTEAD:
x = start_x + rest_indent
continue
elif g.split == SPLIT_BEFORE:
x = start_x + rest_indent
g.x = <short> (x + .5)
if maxx < x + g.width:
maxx = x + g.width
if maxx < x + g.advance:
maxx = x + g.advance
x += g.advance
# Limit us to some width.
if x > MAX_WIDTH:
x = MAX_WIDTH
old_g = g
return maxx
def place_vertical(list glyphs, int y, int spacing, int leading):
"""
Vertically places the non-ruby glyphs. Returns a list of line end heights,
and the y-value for the top of the next line.
"""
cdef Glyph g, gg
cdef int pos, sol, len_glyphs, i
cdef int ascent, line_spacing
cdef bint end_line
if not glyphs:
return [ ], y
len_glyphs = len(glyphs)
pos = 0
sol = 0
ascent = 0
line_spacing = 0
rv = [ ]
y += leading
while True:
if pos >= len_glyphs:
end_line = True
else:
g = glyphs[pos]
end_line = (g.split != SPLIT_NONE)
if end_line:
for i from sol <= i < pos:
gg = glyphs[i]
if gg.ruby == RUBY_TOP:
continue
if gg.ruby == RUBY_ALT:
continue
if gg.ascent:
gg.y = y + ascent
else:
# Glyphs without ascents are displayables, which get
# aligned to the top of the line.
gg.y = y
gg.ascent = ascent
l = Line(y - leading, leading + line_spacing + spacing, glyphs[sol:pos])
rv.append(l)
y += line_spacing
y += spacing
y += leading
sol = pos
ascent = 0
line_spacing = 0
if g.split == SPLIT_INSTEAD:
sol += 1
pos += 1
if pos >= len_glyphs:
break
else:
continue
if pos >= len_glyphs:
break
if g.ascent > ascent:
ascent = g.ascent
if g.line_spacing > line_spacing:
line_spacing = g.line_spacing
pos += 1
rv[-1].eop = True
return rv, y - leading
def kerning(list glyphs, float amount):
cdef Glyph g
for g in glyphs:
g.advance += amount
def assign_times(float t, float gps, list glyphs):
"""
Assign a display time to each glyph.
`t`
The start time of the first glyph.
`gps`
The number of glyphs per second to show.
`glyphs`
A list of glyphs to apply this to.
Returns the time of the first glyph in the next block.
"""
cdef float tpg # time per glyph
cdef Glyph g
if gps == 0:
tpg = 0.0
else:
tpg = 1.0 / gps
for g in glyphs:
if (g.ruby == RUBY_TOP) or (g.ruby == RUBY_ALT):
g.time = -1
continue
t += tpg
g.time = t
return t
def max_times(list l):
"""
Set the max_time filed on each line.
"""
cdef Line line
cdef Glyph g
cdef float max_time
max_time = 0
for line in l:
for g in line.glyphs:
if g.time > max_time:
max_time = g.time
line.max_time = max_time
return max_time
def hyperlink_areas(list l):
"""
Returns a list of (hyperlink, x, y, w, h) tuples, where each entry in
the rectangle represents a contiguous portion of a hyperlink on the
given line.
"""
cdef Line line
cdef Glyph g
cdef list gl
cdef int len_gl
cdef int pos
cdef int max_x
cdef int min_x
cdef int hyperlink
rv = [ ]
for line in l:
gl = line.glyphs
len_gl = len(gl)
hyperlink = 0
max_x = 0
min_x = 1000000
pos = 0
while pos < len_gl:
g = gl[pos]
if (hyperlink and g.hyperlink != hyperlink):
rv.append((hyperlink, min_x, line.y, max_x - min_x, line.height))
hyperlink = 0
max_x = 0
min_x = 1000000
hyperlink = g.hyperlink
if hyperlink:
if g.x < min_x:
min_x = g.x
if g.x + g.width > max_x:
max_x = g.x + <int> g.width
pos += 1
if hyperlink:
rv.append((hyperlink, min_x, line.y, max_x - min_x, line.height))
return rv
def mark_ruby_top(list l):
cdef Glyph g
for g in l:
g.ruby = RUBY_TOP
def mark_altruby_top(list l):
cdef Glyph g
for g in l:
g.ruby = RUBY_ALT
def mark_ruby_bottom(list l):
cdef Glyph g
for g in l:
g.ruby = RUBY_BOTTOM
def place_ruby(list glyphs, int ruby_offset, int altruby_offset, int surf_width, int surf_height):
cdef Glyph g
cdef ruby_t last_ruby = RUBY_NONE
cdef int len_glyphs = len(glyphs)
cdef float x, width, min_x = 0, max_x = 0
cdef int y = 0
cdef int start_top
cdef int pos = 0
cdef int i
while pos < len_glyphs:
g = glyphs[pos]
if g.ruby == RUBY_NONE:
min_x = g.x
max_x = g.x + g.width
y = g.y
last_ruby = RUBY_NONE
pos += 1
continue
elif g.ruby == RUBY_BOTTOM:
if last_ruby != RUBY_BOTTOM:
min_x = g.x
max_x = g.x + g.width
y = g.y
last_ruby = RUBY_BOTTOM
pos += 1
continue
# Otherwise, we have RUBY_TOP or RUBY_ALT
# Find the run of RUBY_TOP. When this is done, the run will be in
# glyphs[start_top:pos].
start_top = pos
last_ruby = g.ruby
while pos < len_glyphs:
g = glyphs[pos]
if g.ruby != last_ruby:
break
pos += 1
# Compute the width of the run.
width = 0
for i from start_top <= i < pos:
g = glyphs[i]
width += g.advance
width -= glyphs[pos - 1].advance
width += glyphs[pos - 1].width
# Place the glyphs.
x = (max_x + min_x) / 2 - width / 2
for i from start_top <= i < pos:
g = glyphs[i]
g.x = <int> (x + .5)
if g.ruby == RUBY_TOP:
g.y = y + ruby_offset
else:
g.y = y + altruby_offset
x += g.advance
def align_and_justify(list lines, int width, float text_align, bint justify):
"""
Handle text alignment and justification.
"""
cdef Line l
cdef Glyph g
cdef int max_x
cdef int spaces
cdef float justify_offset
cdef float justify_per_space
cdef int offset
# See if we have to do anything at all.
if not justify and text_align == 0.0:
return
for l in lines:
spaces = 0
max_x = 0
for g in l.glyphs:
if g.ruby == RUBY_TOP:
continue
if g.ruby == RUBY_ALT:
continue
if g.character == 0x20:
spaces += 1
max_x = <int> (g.x + g.width)
# If we're too big, give up.
if max_x >= MAX_WIDTH:
continue
if justify and spaces and not l.eop:
justify_per_space = 1.0 * (width - max_x) / spaces
justify_offset = 0.5 # Makes numbers round better.
for g in l.glyphs:
if g.ruby == RUBY_TOP:
continue
if g.ruby == RUBY_ALT:
continue
if g.character == 0x20:
justify_offset += justify_per_space
g.x += <int> justify_offset
else:
offset = <int> ((width - max_x) * text_align)
for g in l.glyphs:
if g.ruby == RUBY_TOP:
continue
if g.ruby == RUBY_ALT:
continue
g.x += offset
def reverse_lines(list glyphs):
"""
Reverses each line in glyphs, while keeping the lines themselves in
the original order.
"""
cdef list rv
cdef list block
cdef Glyph g
rv = [ ]
block = [ ]
for g in glyphs:
if g.split == SPLIT_INSTEAD:
block.reverse()
rv.extend(block)
rv.append(g)
block = [ ]
continue
block.append(g)
block.reverse()
rv.extend(block)
return rv
def copy_splits(list source, list dest):
"""
Copies break and timing information from one list of glyphs
to another.
"""
cdef Glyph s
cdef Glyph d
cdef int i
for 0 <= i < len(dest):
s = source[i]
d = dest[i]
d.split = s.split
def tweak_glyph_spacing(list glyphs, list lines, double dx, double dy, double w, double h):
cdef Glyph g
if w <= 0 or h <= 0:
return
cdef short old_x_offset = 0
cdef short x_offset
for g in glyphs:
x_offset = <short> (dx * g.x / w)
g.x += x_offset
g.y += <short> (dy * g.y / h)
if x_offset > old_x_offset:
g.delta_x_offset = x_offset - old_x_offset
old_x_offset = x_offset
for l in lines:
end = l.y + l.height
l.y += int(dy * l.y / h)
end += int(dy * end / h)
l.height = end - l.y
def offset_glyphs(list glyphs, short x, short y):
cdef Glyph g
if x == 0 and y == 0:
return
for g in glyphs:
g.x += x
g.y += y