mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 05:13:58 -07:00
This replaces all links pointing to trac with their corresponding links to gitea. Also replace http with https while at it. Signed-off-by: Ralph Sennhauser <ralph.sennhauser@gmail.com>
243 lines
8.9 KiB
Python
Executable file
243 lines
8.9 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
|
|
import math
|
|
|
|
import cairo
|
|
import font_loader
|
|
import packer
|
|
|
|
|
|
# Representation of a rendered glyph
|
|
class Glyph:
|
|
def __init__(self, ctx, renderstyle, char, idx, face, size):
|
|
self.renderstyle = renderstyle
|
|
self.char = char
|
|
self.idx = idx
|
|
self.face = face
|
|
self.size = size
|
|
self.glyph = (idx, 0, 0)
|
|
|
|
if ctx.get_font_face() != self.face:
|
|
ctx.set_font_face(self.face)
|
|
ctx.set_font_size(self.size)
|
|
extents = ctx.glyph_extents([self.glyph])
|
|
|
|
self.xadvance = round(extents[4])
|
|
|
|
# Find the bounding box of strokes and/or fills:
|
|
|
|
inf = 1e300 * 1e300
|
|
bb = [inf, inf, -inf, -inf]
|
|
|
|
if "stroke" in self.renderstyle:
|
|
for _c, w in self.renderstyle["stroke"]:
|
|
ctx.set_line_width(w)
|
|
ctx.glyph_path([self.glyph])
|
|
e = ctx.stroke_extents()
|
|
bb = (min(bb[0], e[0]), min(bb[1], e[1]), max(bb[2], e[2]), max(bb[3], e[3]))
|
|
ctx.new_path()
|
|
|
|
if "fill" in self.renderstyle:
|
|
ctx.glyph_path([self.glyph])
|
|
e = ctx.fill_extents()
|
|
bb = (min(bb[0], e[0]), min(bb[1], e[1]), max(bb[2], e[2]), max(bb[3], e[3]))
|
|
ctx.new_path()
|
|
|
|
bb = (math.floor(bb[0]), math.floor(bb[1]), math.ceil(bb[2]), math.ceil(bb[3]))
|
|
|
|
self.x0 = -bb[0]
|
|
self.y0 = -bb[1]
|
|
self.w = bb[2] - bb[0]
|
|
self.h = bb[3] - bb[1]
|
|
|
|
def pack(self, packer_instance):
|
|
self.pos = packer_instance.pack(self.w, self.h)
|
|
|
|
def render(self, ctx):
|
|
if ctx.get_font_face() != self.face:
|
|
ctx.set_font_face(self.face)
|
|
ctx.set_font_size(self.size)
|
|
ctx.save()
|
|
ctx.translate(self.x0, self.y0)
|
|
ctx.translate(self.pos.x, self.pos.y)
|
|
|
|
# Render each stroke, and then each fill on top of it
|
|
|
|
if "stroke" in self.renderstyle:
|
|
for (r, g, b, a), w in self.renderstyle["stroke"]:
|
|
ctx.set_line_width(w)
|
|
ctx.set_source_rgba(r, g, b, a)
|
|
ctx.glyph_path([self.glyph])
|
|
ctx.stroke()
|
|
|
|
if "fill" in self.renderstyle:
|
|
for r, g, b, a in self.renderstyle["fill"]:
|
|
ctx.set_source_rgba(r, g, b, a)
|
|
ctx.glyph_path([self.glyph])
|
|
ctx.fill()
|
|
|
|
ctx.restore()
|
|
|
|
|
|
# Load the set of characters contained in the given text file
|
|
def load_char_list(filename):
|
|
with open(filename, encoding="utf-8") as f:
|
|
chars = f.read()
|
|
return set(chars)
|
|
|
|
|
|
# Construct a Cairo context and surface for rendering text with the given parameters
|
|
def setup_context(width, height, renderstyle):
|
|
surface_format = cairo.FORMAT_ARGB32 if "colour" in renderstyle else cairo.FORMAT_A8
|
|
surface = cairo.ImageSurface(surface_format, width, height)
|
|
ctx = cairo.Context(surface)
|
|
ctx.set_line_join(cairo.LINE_JOIN_ROUND)
|
|
return ctx, surface
|
|
|
|
|
|
def generate_font(outname, ttf_names, loadopts, size, renderstyle, dsizes):
|
|
face_list = []
|
|
index_list = []
|
|
for i in range(len(ttf_names)):
|
|
(face, indices) = font_loader.create_cairo_font_face_for_file(
|
|
f"../../../binaries/data/tools/fontbuilder/fonts/{ttf_names[i]}", 0, loadopts
|
|
)
|
|
face_list.append(face)
|
|
if ttf_names[i] not in dsizes:
|
|
dsizes[ttf_names[i]] = 0
|
|
index_list.append(indices)
|
|
|
|
(ctx, _) = setup_context(1, 1, renderstyle)
|
|
|
|
# TODO: this gets the line height from the default font
|
|
# while entire texts can be in the fallback font
|
|
ctx.set_font_face(face_list[0])
|
|
ctx.set_font_size(size + dsizes[ttf_names[0]])
|
|
(_, _, linespacing, _, _) = ctx.font_extents()
|
|
|
|
# Estimate the 'average' height of text, for vertical center alignment
|
|
charheight = round(ctx.glyph_extents([(index_list[0]("I"), 0.0, 0.0)])[3])
|
|
|
|
# Translate all the characters into glyphs
|
|
# (This is inefficient if multiple characters have the same glyph)
|
|
glyphs = []
|
|
# for c in chars:
|
|
for c in range(0x20, 0xFFFE):
|
|
for i in range(len(index_list)):
|
|
idx = index_list[i](chr(c))
|
|
if c == 0xFFFD and idx == 0: # use "?" if the missing-glyph glyph is missing
|
|
idx = index_list[i]("?")
|
|
if idx:
|
|
glyphs.append(
|
|
Glyph(ctx, renderstyle, chr(c), idx, face_list[i], size + dsizes[ttf_names[i]])
|
|
)
|
|
break
|
|
|
|
# Sort by decreasing height (tie-break on decreasing width)
|
|
glyphs.sort(key=lambda g: (-g.h, -g.w))
|
|
|
|
# Try various sizes to pack the glyphs into
|
|
sizes = []
|
|
for h in [32, 64, 128, 256, 512, 1024, 2048, 4096]:
|
|
sizes.append((h, h))
|
|
sizes.append((h * 2, h))
|
|
sizes.sort(
|
|
key=lambda w_h: (w_h[0] * w_h[1], max(w_h[0], w_h[1]))
|
|
) # prefer smaller and squarer
|
|
|
|
for w, h in sizes:
|
|
try:
|
|
# Using the dump pacher usually creates bigger textures, but runs faster
|
|
# In practice the size difference is so small it always ends up in the same size
|
|
packer_instance = packer.DumbRectanglePacker(w, h)
|
|
for g in glyphs:
|
|
g.pack(packer_instance)
|
|
except packer.OutOfSpaceError:
|
|
continue
|
|
|
|
ctx, surface = setup_context(w, h, renderstyle)
|
|
for g in glyphs:
|
|
g.render(ctx)
|
|
surface.write_to_png(f"{outname}.png")
|
|
|
|
# Output the .fnt file with all the glyph positions etc
|
|
with open(f"{outname}.fnt", "w", encoding="utf-8") as fnt:
|
|
fnt.write("101\n")
|
|
fnt.write("%d %d\n" % (w, h))
|
|
fnt.write("%s\n" % ("rgba" if "colour" in renderstyle else "a"))
|
|
fnt.write("%d\n" % len(glyphs))
|
|
fnt.write("%d\n" % linespacing)
|
|
fnt.write("%d\n" % charheight)
|
|
for g in glyphs:
|
|
x0 = g.x0
|
|
y0 = g.y0
|
|
# UGLY HACK: see https://gitea.wildfiregames.com/0ad/0ad/issues/1039 ;
|
|
# to handle a-macron-acute characters without the hassle of
|
|
# doing proper OpenType GPOS layout (which the font
|
|
# doesn't support anyway), we'll just shift the combining acute
|
|
# glyph by an arbitrary amount to make it roughly the right
|
|
# place when used after an a-macron glyph.
|
|
if ord(g.char) == 0x0301:
|
|
y0 += charheight / 3
|
|
|
|
fnt.write(
|
|
"%d %d %d %d %d %d %d %d\n"
|
|
% (ord(g.char), g.pos.x, h - g.pos.y, g.w, g.h, -x0, y0, g.xadvance)
|
|
)
|
|
|
|
return
|
|
print("Failed to fit glyphs in texture")
|
|
|
|
|
|
filled = {"fill": [(1, 1, 1, 1)]}
|
|
stroked1 = {
|
|
"colour": True,
|
|
"stroke": [((0, 0, 0, 1), 2.0), ((0, 0, 0, 1), 2.0)],
|
|
"fill": [(1, 1, 1, 1)],
|
|
}
|
|
stroked2 = {"colour": True, "stroke": [((0, 0, 0, 1), 2.0)], "fill": [(1, 1, 1, 1), (1, 1, 1, 1)]}
|
|
stroked3 = {"colour": True, "stroke": [((0, 0, 0, 1), 2.5)], "fill": [(1, 1, 1, 1), (1, 1, 1, 1)]}
|
|
|
|
# For extra glyph support, add your preferred font to the font array
|
|
Sans = (["LinBiolinum_Rah.ttf", "FreeSans.ttf"], font_loader.FT_LOAD_DEFAULT)
|
|
Sans_Bold = (["LinBiolinum_RBah.ttf", "FreeSansBold.ttf"], font_loader.FT_LOAD_DEFAULT)
|
|
Sans_Italic = (["LinBiolinum_RIah.ttf", "FreeSansOblique.ttf"], font_loader.FT_LOAD_DEFAULT)
|
|
SansMono = (["DejaVuSansMono.ttf", "FreeMono.ttf"], font_loader.FT_LOAD_DEFAULT)
|
|
Serif = (["texgyrepagella-regular.otf", "FreeSerif.ttf"], font_loader.FT_LOAD_NO_HINTING)
|
|
Serif_Bold = (["texgyrepagella-bold.otf", "FreeSerifBold.ttf"], font_loader.FT_LOAD_NO_HINTING)
|
|
|
|
# Define the size differences used to render different fallback fonts
|
|
# I.e. when adding a fallback font has smaller glyphs than the original, you can bump it
|
|
dsizes = {"HanaMinA.ttf": 2} # make the glyphs for the (chinese font 2 pts bigger)
|
|
|
|
fonts = (
|
|
("mono-10", SansMono, 10, filled),
|
|
("mono-stroke-10", SansMono, 10, stroked2),
|
|
("sans-9", Sans, 9, filled),
|
|
("sans-10", Sans, 10, filled),
|
|
("sans-12", Sans, 12, filled),
|
|
("sans-13", Sans, 13, filled),
|
|
("sans-14", Sans, 14, filled),
|
|
("sans-16", Sans, 16, filled),
|
|
("sans-bold-12", Sans_Bold, 12, filled),
|
|
("sans-bold-13", Sans_Bold, 13, filled),
|
|
("sans-bold-14", Sans_Bold, 14, filled),
|
|
("sans-bold-16", Sans_Bold, 16, filled),
|
|
("sans-bold-18", Sans_Bold, 18, filled),
|
|
("sans-bold-20", Sans_Bold, 20, filled),
|
|
("sans-bold-22", Sans_Bold, 22, filled),
|
|
("sans-bold-24", Sans_Bold, 24, filled),
|
|
("sans-stroke-12", Sans, 12, stroked2),
|
|
("sans-bold-stroke-12", Sans_Bold, 12, stroked3),
|
|
("sans-stroke-13", Sans, 13, stroked2),
|
|
("sans-bold-stroke-13", Sans_Bold, 13, stroked3),
|
|
("sans-stroke-14", Sans, 14, stroked2),
|
|
("sans-bold-stroke-14", Sans_Bold, 14, stroked3),
|
|
("sans-stroke-16", Sans, 16, stroked2),
|
|
)
|
|
|
|
for name, (fontnames, loadopts), size, style in fonts:
|
|
print(f"{name}...")
|
|
generate_font(
|
|
f"../../../binaries/data/mods/mod/fonts/{name}", fontnames, loadopts, size, style, dsizes
|
|
)
|