0ad/source/tools/fontbuilder2/fontbuilder.py
Dunedan bcf97b608b
Enable ruff rules for docstrings and comments
This enables some ruff rules for docstrings and comments. The idea is to
not enforce the presence of docstrings, but to ensure they are properly
formatted if they're present.

For comments this adds checks that they don't contain code and verify
the formatting of comments with "TODO" tags.

As part of this, some commented out code which hasn't been touch in the
past 10 years gets removed as well.

The rules enabled enabled by this are:

- check formatting of existing docstrings (D200-)
- check comments for code (ERA)
- check formatting of TODO tags (TD001, TD004-)
2024-08-31 21:09:20 +02:00

243 lines
8.9 KiB
Python
Executable file

#!/usr/bin/env python3
import math
import cairo
import FontLoader
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):
self.pos = packer.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, ttfNames, loadopts, size, renderstyle, dsizes):
faceList = []
indexList = []
for i in range(len(ttfNames)):
(face, indices) = FontLoader.create_cairo_font_face_for_file(
f"../../../binaries/data/tools/fontbuilder/fonts/{ttfNames[i]}", 0, loadopts
)
faceList.append(face)
if ttfNames[i] not in dsizes:
dsizes[ttfNames[i]] = 0
indexList.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(faceList[0])
ctx.set_font_size(size + dsizes[ttfNames[0]])
(_, _, linespacing, _, _) = ctx.font_extents()
# Estimate the 'average' height of text, for vertical center alignment
charheight = round(ctx.glyph_extents([(indexList[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(indexList)):
idx = indexList[i](chr(c))
if c == 0xFFFD and idx == 0: # use "?" if the missing-glyph glyph is missing
idx = indexList[i]("?")
if idx:
glyphs.append(
Glyph(ctx, renderstyle, chr(c), idx, faceList[i], size + dsizes[ttfNames[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 = Packer.DumbRectanglePacker(w, h)
for g in glyphs:
g.pack(packer)
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 http://trac.wildfiregames.com/ticket/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"], FontLoader.FT_LOAD_DEFAULT)
Sans_Bold = (["LinBiolinum_RBah.ttf", "FreeSansBold.ttf"], FontLoader.FT_LOAD_DEFAULT)
Sans_Italic = (["LinBiolinum_RIah.ttf", "FreeSansOblique.ttf"], FontLoader.FT_LOAD_DEFAULT)
SansMono = (["DejaVuSansMono.ttf", "FreeMono.ttf"], FontLoader.FT_LOAD_DEFAULT)
Serif = (["texgyrepagella-regular.otf", "FreeSerif.ttf"], FontLoader.FT_LOAD_NO_HINTING)
Serif_Bold = (["texgyrepagella-bold.otf", "FreeSerifBold.ttf"], FontLoader.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
)