fonttools/Lib/fontTools/varLib/interpolatablePlot.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

394 lines
14 KiB
Python
Raw Normal View History

from fontTools.pens.recordingPen import RecordingPen
from fontTools.pens.boundsPen import ControlBoundsPen
from fontTools.pens.cairoPen import CairoPen
from fontTools.pens.pointPen import SegmentToPointPen
from fontTools.varLib.interpolatable import PerContourOrComponentPen, RecordingPointPen
2023-11-15 19:52:51 -07:00
from itertools import cycle
import cairo
2023-11-16 14:58:48 -07:00
import math
class InterpolatablePdf:
width = 640
height = 480
pad = 16
line_height = 36
head_color = (0.3, 0.3, 0.3)
label_color = (0.2, 0.2, 0.2)
border_color = (0.9, 0.9, 0.9)
border_width = 1
fill_color = (0.8, 0.8, 0.8)
stroke_color = (0.1, 0.1, 0.1)
stroke_width = 2
2023-11-15 19:56:17 -07:00
oncurve_node_color = (0, 0.8, 0)
oncurve_node_diameter = 10
2023-11-15 19:56:17 -07:00
offcurve_node_color = (0, 0.5, 0)
offcurve_node_diameter = 8
2023-11-15 19:56:17 -07:00
handle_color = (0.2, 1, 0.2)
handle_width = 1
other_start_point_color = (0, 0, 1)
reversed_start_point_color = (0, 1, 0)
start_point_color = (1, 0, 0)
start_point_width = 15
start_handle_width = 5
2023-11-16 14:58:48 -07:00
start_handle_length = 100
start_handle_arrow_length = 5
2023-11-15 19:52:51 -07:00
contour_colors = ((1, 0, 0), (0, 0, 1), (0, 1, 0), (1, 1, 0), (1, 0, 1), (0, 1, 1))
contour_alpha = 0.5
cupcake_color = (0.3, 0, 0.3)
cupcake = r"""
,@.
,@.@@,.
,@@,.@@@. @.@@@,.
,@@. @@@. @@. @@,.
,@@@.@,.@. @. @@@@,.@.@@,.
,@@.@. @@.@@. @,. .@ @ @@,
,@@. @. .@@.@@@. @@ @,
,@. @@. @,
@. @,@@,. , .@@,
@,. .@,@@,. .@@,. , .@@, @, @,
@. .@. @ @@,. , @
@,.@@. @,. @@,. @. @,. @
@@||@,. @@,. @@,. @@ @,. @@@, @
\\@@@@ @,. @@@@@ @@,. @@@ //@@@
|||||||| @@,. @@ ||||||| |@@@|@|| ||
\\\\\\\ ||@@@|| ||||||| ||||||| //
||||||| |||||| |||||| |||||| ||
\\\\\\ |||||| |||||| |||||| //
|||||| ||||| ||||| ||||| ||
\\\\\ ||||| ||||| ||||| //
||||| |||| ||||| |||| ||
\\\\ |||| |||| |||| //
||||||||||||||||||||||||
"""
shrug_color = (0, 0.3, 0.3)
shrug = r"""¯\_(")_/¯"""
def __init__(self, outfile, glyphsets, names=None, **kwargs):
self.outfile = outfile
self.glyphsets = glyphsets
self.names = names or [repr(g) for g in glyphsets]
for k, v in kwargs.items():
if not hasattr(self, k):
raise TypeError("Unknown keyword argument: %s" % k)
setattr(self, k, v)
def __enter__(self):
self.surface = cairo.PDFSurface(self.outfile, self.width, self.height)
return self
def __exit__(self, type, value, traceback):
self.surface.finish()
def add_problems(self, problems):
for glyph, glyph_problems in problems.items():
for p in glyph_problems:
self.add_problem(glyph, p)
def add_problem(self, glyphname, p):
master_keys = ("master",) if "master" in p else ("master_1", "master_2")
master_indices = [self.names.index(p[k]) for k in master_keys]
if p["type"] == "missing":
sample_glyph = next(
i for i, m in enumerate(self.glyphsets) if m[glyphname] is not None
)
master_indices.insert(0, sample_glyph)
total_width = self.width + 2 * self.pad
total_height = (
self.pad
+ self.line_height
+ self.pad
+ len(master_indices) * (self.height + self.pad * 2 + self.line_height)
+ self.pad
)
self.surface.set_size(total_width, total_height)
x = self.pad
y = self.pad
self.draw_label(glyphname, y=y, color=self.head_color, align=0)
self.draw_label(p["type"], y=y, color=self.head_color, align=1)
y += self.line_height + self.pad
for which, master_idx in enumerate(master_indices):
glyphset = self.glyphsets[master_idx]
name = self.names[master_idx]
self.draw_label(name, y=y, color=self.label_color, align=0.5)
y += self.line_height + self.pad
if glyphset[glyphname] is not None:
self.draw_glyph(glyphset, glyphname, p, which, x=x, y=y)
else:
self.draw_shrug(x=x, y=y)
y += self.height + self.pad
self.surface.show_page()
def draw_label(self, label, *, y=0, color=(0, 0, 0), align=0):
cr = cairo.Context(self.surface)
cr.select_font_face("@cairo", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
cr.set_font_size(self.line_height)
font_extents = cr.font_extents()
font_size = self.line_height * self.line_height / font_extents[2]
cr.set_font_size(font_size)
font_extents = cr.font_extents()
cr.set_source_rgb(*color)
extents = cr.text_extents(label)
if extents.width > self.width:
# Shrink
font_size *= self.width / extents.width
cr.set_font_size(font_size)
font_extents = cr.font_extents()
extents = cr.text_extents(label)
# Center
label_x = (self.width - extents.width) * align + self.pad
label_y = y + font_extents[0]
cr.move_to(label_x, label_y)
cr.show_text(label)
def draw_glyph(self, glyphset, glyphname, problem, which, *, x=0, y=0):
problem_type = problem["type"]
glyph = glyphset[glyphname]
recording = RecordingPen()
glyph.draw(recording)
boundsPen = ControlBoundsPen(glyphset)
recording.replay(boundsPen)
glyph_width = boundsPen.bounds[2] - boundsPen.bounds[0]
glyph_height = boundsPen.bounds[3] - boundsPen.bounds[1]
scale = min(self.width / glyph_width, self.height / glyph_height)
cr = cairo.Context(self.surface)
cr.translate(x, y)
# Center
cr.translate(
(self.width - glyph_width * scale) / 2,
(self.height - glyph_height * scale) / 2,
)
cr.scale(scale, -scale)
cr.translate(-boundsPen.bounds[0], -boundsPen.bounds[3])
if self.border_color:
cr.set_source_rgb(*self.border_color)
cr.rectangle(
boundsPen.bounds[0], boundsPen.bounds[1], glyph_width, glyph_height
)
cr.set_line_width(self.border_width / scale)
cr.stroke()
if self.fill_color and problem_type != "open_path":
pen = CairoPen(glyphset, cr)
recording.replay(pen)
cr.set_source_rgb(*self.fill_color)
cr.fill()
if self.stroke_color:
pen = CairoPen(glyphset, cr)
recording.replay(pen)
cr.set_source_rgb(*self.stroke_color)
cr.set_line_width(self.stroke_width / scale)
cr.stroke()
if problem_type in ("node_count", "node_incompatibility"):
cr.set_line_cap(cairo.LINE_CAP_ROUND)
# Oncurve nodes
for segment, args in recording.value:
if not args:
continue
x, y = args[-1]
cr.move_to(x, y)
cr.line_to(x, y)
cr.set_source_rgb(*self.oncurve_node_color)
cr.set_line_width(self.oncurve_node_diameter / scale)
cr.stroke()
# Offcurve nodes
for segment, args in recording.value:
for x, y in args[:-1]:
cr.move_to(x, y)
cr.line_to(x, y)
cr.set_source_rgb(*self.offcurve_node_color)
cr.set_line_width(self.offcurve_node_diameter / scale)
cr.stroke()
# Handles
for segment, args in recording.value:
if not args:
pass
2023-11-15 19:56:17 -07:00
elif segment in ("moveTo", "lineTo"):
cr.move_to(*args[0])
elif segment == "qCurveTo":
for x, y in args:
cr.line_to(x, y)
cr.new_sub_path()
2023-11-15 19:56:17 -07:00
cr.move_to(*args[-1])
elif segment == "curveTo":
cr.line_to(*args[0])
cr.new_sub_path()
cr.move_to(*args[1])
cr.line_to(*args[2])
cr.new_sub_path()
2023-11-15 19:56:17 -07:00
cr.move_to(*args[-1])
else:
assert False
cr.set_source_rgb(*self.handle_color)
cr.set_line_width(self.handle_width / scale)
cr.stroke()
if problem_type == "wrong_start_point":
idx = problem["contour"]
# Draw suggested point
if which == 0:
perContourPen = PerContourOrComponentPen(
RecordingPen, glyphset=glyphset
)
recording.replay(perContourPen)
points = RecordingPointPen()
converter = SegmentToPointPen(points, False)
perContourPen.value[idx].replay(converter)
targetPoint = points.value[problem["value_2"]][0]
cr.move_to(*targetPoint)
cr.line_to(*targetPoint)
cr.set_line_cap(cairo.LINE_CAP_ROUND)
cr.set_source_rgb(*self.other_start_point_color)
cr.set_line_width(self.start_point_width / scale)
cr.stroke()
2023-11-16 14:58:48 -07:00
# Draw start point
cr.set_line_cap(cairo.LINE_CAP_ROUND)
i = 0
for segment, args in recording.value:
if segment == "moveTo":
if i == idx:
cr.move_to(*args[0])
cr.line_to(*args[0])
i += 1
if which == 0 or not problem["reversed"]:
cr.set_source_rgb(*self.start_point_color)
else:
cr.set_source_rgb(*self.reversed_start_point_color)
cr.set_line_width(self.start_point_width / scale)
cr.stroke()
2023-11-16 14:58:48 -07:00
# Draw arrow
cr.set_line_cap(cairo.LINE_CAP_SQUARE)
first_pt = None
i = 0
for segment, args in recording.value:
2023-11-15 19:56:17 -07:00
if segment == "moveTo":
first_pt = args[0]
continue
if first_pt is None:
continue
second_pt = args[0]
if i == idx:
2023-11-16 14:58:48 -07:00
first_pt = complex(*first_pt)
second_pt = complex(*second_pt)
length = abs(second_pt - first_pt)
if length:
# Draw handle
length *= scale
second_pt = (
first_pt
+ (second_pt - first_pt) / length * self.start_handle_length
)
cr.move_to(first_pt.real, first_pt.imag)
cr.line_to(second_pt.real, second_pt.imag)
# Draw arrowhead
cr.save()
cr.translate(second_pt.real, second_pt.imag)
cr.rotate(
math.atan2(
second_pt.imag - first_pt.imag,
second_pt.real - first_pt.real,
)
)
2023-11-16 15:04:13 -07:00
cr.translate(self.start_handle_width, 0)
2023-11-16 14:58:48 -07:00
cr.move_to(0, 0)
cr.scale(1 / scale, 1 / scale)
cr.line_to(
-self.start_handle_arrow_length,
-self.start_handle_arrow_length,
)
cr.line_to(
-self.start_handle_arrow_length,
self.start_handle_arrow_length,
)
cr.close_path()
cr.restore()
first_pt = None
i += 1
cr.set_line_width(self.start_handle_width / scale)
cr.stroke()
2023-11-15 19:52:51 -07:00
if problem_type == "contour_order":
matching = problem["value_2"]
colors = cycle(self.contour_colors)
2023-11-15 19:56:17 -07:00
perContourPen = PerContourOrComponentPen(RecordingPen, glyphset=glyphset)
2023-11-15 19:52:51 -07:00
recording.replay(perContourPen)
for i, contour in enumerate(perContourPen.value):
if matching[i] == i:
continue
color = next(colors)
2023-11-15 19:52:51 -07:00
contour.replay(CairoPen(glyphset, cr))
cr.set_source_rgba(*color, self.contour_alpha)
cr.fill()
def draw_cupcake(self):
cupcake = self.cupcake.splitlines()
cr = cairo.Context(self.surface)
cr.set_source_rgb(*self.cupcake_color)
cr.set_font_size(self.line_height)
cr.select_font_face(
"monospace", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL
)
width = 0
height = 0
for line in cupcake:
extents = cr.text_extents(line)
width = max(width, extents.width)
height += extents.height
if not width:
return
cr.scale(self.width / width, self.height / height)
for line in cupcake:
cr.translate(0, cr.text_extents(line).height)
cr.move_to(0, 0)
cr.show_text(line)
def draw_shrug(self, x=0, y=0):
cr = cairo.Context(self.surface)
cr.translate(x, y)
cr.set_source_rgb(*self.shrug_color)
cr.set_font_size(self.line_height)
cr.select_font_face(
"monospace", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL
)
extents = cr.text_extents(self.shrug)
if not extents.width:
return
cr.translate(0, self.height * 0.6)
scale = self.width / extents.width
cr.scale(scale, scale)
cr.move_to(-extents.x_bearing, 0)
cr.show_text(self.shrug)