682 lines
24 KiB
Python
682 lines
24 KiB
Python
from fontTools.pens.recordingPen import (
|
||
RecordingPen,
|
||
DecomposingRecordingPen,
|
||
RecordingPointPen,
|
||
)
|
||
from fontTools.pens.boundsPen import ControlBoundsPen
|
||
from fontTools.pens.cairoPen import CairoPen
|
||
from fontTools.pens.pointPen import (
|
||
SegmentToPointPen,
|
||
PointToSegmentPen,
|
||
ReverseContourPointPen,
|
||
)
|
||
from fontTools.varLib.interpolatable import (
|
||
PerContourOrComponentPen,
|
||
SimpleRecordingPointPen,
|
||
)
|
||
from itertools import cycle
|
||
from functools import wraps
|
||
from io import BytesIO
|
||
import cairo
|
||
import math
|
||
import logging
|
||
|
||
log = logging.getLogger("fontTools.varLib.interpolatable")
|
||
|
||
|
||
class LerpGlyphSet:
|
||
def __init__(self, glyphset1, glyphset2, factor=0.5):
|
||
self.glyphset1 = glyphset1
|
||
self.glyphset2 = glyphset2
|
||
self.factor = factor
|
||
|
||
def __getitem__(self, glyphname):
|
||
return LerpGlyph(glyphname, self)
|
||
|
||
|
||
class LerpGlyph:
|
||
def __init__(self, glyphname, glyphset):
|
||
self.glyphset = glyphset
|
||
self.glyphname = glyphname
|
||
|
||
def draw(self, pen):
|
||
recording1 = DecomposingRecordingPen(self.glyphset.glyphset1)
|
||
self.glyphset.glyphset1[self.glyphname].draw(recording1)
|
||
recording2 = DecomposingRecordingPen(self.glyphset.glyphset2)
|
||
self.glyphset.glyphset2[self.glyphname].draw(recording2)
|
||
|
||
factor = self.glyphset.factor
|
||
for (op1, args1), (op2, args2) in zip(recording1.value, recording2.value):
|
||
if op1 != op2:
|
||
raise ValueError("Mismatching operations: %s, %s" % (op1, op2))
|
||
mid_args = [
|
||
(x1 + (x2 - x1) * factor, y1 + (y2 - y1) * factor)
|
||
for (x1, y1), (x2, y2) in zip(args1, args2)
|
||
]
|
||
getattr(pen, op1)(*mid_args)
|
||
|
||
|
||
class OverridingDict(dict):
|
||
def __init__(self, parent_dict):
|
||
self.parent_dict = parent_dict
|
||
|
||
def __missing__(self, key):
|
||
return self.parent_dict[key]
|
||
|
||
|
||
class InterpolatablePlot:
|
||
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
|
||
oncurve_node_color = (0, 0.8, 0)
|
||
oncurve_node_diameter = 10
|
||
offcurve_node_color = (0, 0.5, 0)
|
||
offcurve_node_diameter = 8
|
||
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
|
||
start_handle_length = 100
|
||
start_handle_arrow_length = 5
|
||
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, out, glyphsets, names=None, **kwargs):
|
||
self.out = out
|
||
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):
|
||
return self
|
||
|
||
def __exit__(self, type, value, traceback):
|
||
pass
|
||
|
||
def set_size(self, width, height):
|
||
raise NotImplementedError
|
||
|
||
def show_page(self):
|
||
raise NotImplementedError
|
||
|
||
def add_problems(self, problems):
|
||
for glyph, glyph_problems in problems.items():
|
||
last_masters = None
|
||
current_glyph_problems = []
|
||
for p in glyph_problems:
|
||
masters = (
|
||
p["master_idx"]
|
||
if "master_idx" in p
|
||
else (p["master_1_idx"], p["master_2_idx"])
|
||
)
|
||
if masters == last_masters:
|
||
current_glyph_problems.append(p)
|
||
continue
|
||
# Flush
|
||
if current_glyph_problems:
|
||
self.add_problem(glyph, current_glyph_problems)
|
||
self.show_page()
|
||
current_glyph_problems = []
|
||
last_masters = masters
|
||
current_glyph_problems.append(p)
|
||
if current_glyph_problems:
|
||
self.add_problem(glyph, current_glyph_problems)
|
||
self.show_page()
|
||
|
||
def add_problem(self, glyphname, problems):
|
||
if type(problems) not in (list, tuple):
|
||
problems = [problems]
|
||
|
||
problem_type = problems[0]["type"]
|
||
problem_types = set(problem["type"] for problem in problems)
|
||
if not all(pt == problem_type for pt in problem_types):
|
||
problem_type = ", ".join(sorted({problem["type"] for problem in problems}))
|
||
|
||
log.info("Drawing %s: %s", glyphname, problem_type)
|
||
|
||
master_keys = (
|
||
("master_idx",)
|
||
if "master_idx" in problems[0]
|
||
else ("master_1_idx", "master_2_idx")
|
||
)
|
||
master_indices = [problems[0][k] for k in master_keys]
|
||
|
||
if problem_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 + 3 * 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.set_size(total_width, total_height)
|
||
|
||
x = self.pad
|
||
y = self.pad
|
||
|
||
self.draw_label(glyphname, x=x, y=y, color=self.head_color, align=0, bold=True)
|
||
self.draw_label(
|
||
problem_type,
|
||
x=x + self.width + self.pad,
|
||
y=y,
|
||
color=self.head_color,
|
||
align=1,
|
||
bold=True,
|
||
)
|
||
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, x=x, 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, problems, which, x=x, y=y)
|
||
else:
|
||
self.draw_shrug(x=x, y=y)
|
||
y += self.height + self.pad
|
||
|
||
if any(
|
||
pt in ("nothing", "wrong_start_point", "contour_order")
|
||
for pt in problem_types
|
||
):
|
||
x = self.pad + self.width + self.pad
|
||
y = self.pad
|
||
y += self.line_height + self.pad
|
||
|
||
glyphset1 = self.glyphsets[master_indices[0]]
|
||
glyphset2 = self.glyphsets[master_indices[1]]
|
||
|
||
# Draw the mid-way of the two masters
|
||
|
||
self.draw_label(
|
||
"midway interpolation", x=x, y=y, color=self.head_color, align=0.5
|
||
)
|
||
y += self.line_height + self.pad
|
||
|
||
midway_glyphset = LerpGlyphSet(glyphset1, glyphset2)
|
||
self.draw_glyph(
|
||
midway_glyphset, glyphname, {"type": "midway"}, None, x=x, y=y
|
||
)
|
||
y += self.height + self.pad
|
||
|
||
# Draw the fixed mid-way of the two masters
|
||
|
||
self.draw_label("proposed fix", x=x, y=y, color=self.head_color, align=0.5)
|
||
y += self.line_height + self.pad
|
||
|
||
overriding1 = OverridingDict(glyphset1)
|
||
overriding2 = OverridingDict(glyphset2)
|
||
perContourPen1 = PerContourOrComponentPen(
|
||
RecordingPen, glyphset=overriding1
|
||
)
|
||
perContourPen2 = PerContourOrComponentPen(
|
||
RecordingPen, glyphset=overriding2
|
||
)
|
||
glyphset1[glyphname].draw(perContourPen1)
|
||
glyphset2[glyphname].draw(perContourPen2)
|
||
|
||
for problem in problems:
|
||
if problem["type"] == "contour_order":
|
||
fixed_contours = [
|
||
perContourPen2.value[i] for i in problems[0]["value_2"]
|
||
]
|
||
perContourPen2.value = fixed_contours
|
||
|
||
for problem in problems:
|
||
if problem["type"] == "wrong_start_point":
|
||
# Save the wrong contours
|
||
wrongContour1 = perContourPen1.value[problem["contour"]]
|
||
wrongContour2 = perContourPen2.value[problem["contour"]]
|
||
|
||
# Convert the wrong contours to point pens
|
||
points1 = RecordingPointPen()
|
||
converter = SegmentToPointPen(points1, False)
|
||
wrongContour1.replay(converter)
|
||
points2 = RecordingPointPen()
|
||
converter = SegmentToPointPen(points2, False)
|
||
wrongContour2.replay(converter)
|
||
|
||
proposed_start = problem["value_2"]
|
||
|
||
# See if we need reversing; fragile but worth a try
|
||
if problem["reversed"]:
|
||
new_points2 = RecordingPointPen()
|
||
reversedPen = ReverseContourPointPen(new_points2)
|
||
points2.replay(reversedPen)
|
||
points2 = new_points2
|
||
proposed_start = len(points2.value) - 2 - proposed_start
|
||
|
||
# Rotate points2 so that the first point is the same as in points1
|
||
beginPath = points2.value[:1]
|
||
endPath = points2.value[-1:]
|
||
pts = points2.value[1:-1]
|
||
pts = pts[proposed_start:] + pts[:proposed_start]
|
||
points2.value = beginPath + pts + endPath
|
||
|
||
# Convert the point pens back to segment pens
|
||
segment1 = RecordingPen()
|
||
converter = PointToSegmentPen(segment1, True)
|
||
points1.replay(converter)
|
||
segment2 = RecordingPen()
|
||
converter = PointToSegmentPen(segment2, True)
|
||
points2.replay(converter)
|
||
|
||
# Replace the wrong contours
|
||
wrongContour1.value = segment1.value
|
||
wrongContour2.value = segment2.value
|
||
|
||
# Assemble
|
||
fixed1 = RecordingPen()
|
||
fixed2 = RecordingPen()
|
||
for contour in perContourPen1.value:
|
||
fixed1.value.extend(contour.value)
|
||
for contour in perContourPen2.value:
|
||
fixed2.value.extend(contour.value)
|
||
fixed1.draw = fixed1.replay
|
||
fixed2.draw = fixed2.replay
|
||
|
||
overriding1[glyphname] = fixed1
|
||
overriding2[glyphname] = fixed2
|
||
|
||
try:
|
||
midway_glyphset = LerpGlyphSet(overriding1, overriding2)
|
||
self.draw_glyph(
|
||
midway_glyphset, glyphname, {"type": "fixed"}, None, x=x, y=y
|
||
)
|
||
except ValueError:
|
||
self.draw_shrug(x=x, y=y)
|
||
y += self.height + self.pad
|
||
|
||
def draw_label(self, label, *, x, y, color=(0, 0, 0), align=0, bold=False):
|
||
cr = cairo.Context(self.surface)
|
||
cr.select_font_face(
|
||
"@cairo:",
|
||
cairo.FONT_SLANT_NORMAL,
|
||
cairo.FONT_WEIGHT_BOLD if bold else 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 = x + (self.width - extents.width) * align
|
||
label_y = y + font_extents[0]
|
||
cr.move_to(label_x, label_y)
|
||
cr.show_text(label)
|
||
|
||
def draw_glyph(self, glyphset, glyphname, problems, which, *, x=0, y=0):
|
||
if type(problems) not in (list, tuple):
|
||
problems = [problems]
|
||
|
||
problem_type = problems[0]["type"]
|
||
problem_types = set(problem["type"] for problem in problems)
|
||
if not all(pt == problem_type for pt in problem_types):
|
||
problem_type = "mixed"
|
||
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 ("nothing", "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
|
||
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()
|
||
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()
|
||
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()
|
||
|
||
matching = None
|
||
for problem in problems:
|
||
if problem["type"] == "contour_order":
|
||
matching = problem["value_2"]
|
||
colors = cycle(self.contour_colors)
|
||
perContourPen = PerContourOrComponentPen(
|
||
RecordingPen, glyphset=glyphset
|
||
)
|
||
recording.replay(perContourPen)
|
||
for i, contour in enumerate(perContourPen.value):
|
||
if matching[i] == i:
|
||
continue
|
||
color = next(colors)
|
||
contour.replay(CairoPen(glyphset, cr))
|
||
cr.set_source_rgba(*color, self.contour_alpha)
|
||
cr.fill()
|
||
|
||
for problem in problems:
|
||
if problem["type"] in ("nothing", "wrong_start_point"):
|
||
idx = problem.get("contour")
|
||
|
||
# Draw suggested point
|
||
if idx is not None and which == 1:
|
||
perContourPen = PerContourOrComponentPen(
|
||
RecordingPen, glyphset=glyphset
|
||
)
|
||
recording.replay(perContourPen)
|
||
points = SimpleRecordingPointPen()
|
||
converter = SegmentToPointPen(points, False)
|
||
perContourPen.value[
|
||
idx if matching is None else matching[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()
|
||
|
||
# Draw start point
|
||
cr.set_line_cap(cairo.LINE_CAP_ROUND)
|
||
i = 0
|
||
for segment, args in recording.value:
|
||
if segment == "moveTo":
|
||
if idx is None or i == idx:
|
||
cr.move_to(*args[0])
|
||
cr.line_to(*args[0])
|
||
i += 1
|
||
|
||
if which == 0 or not problem.get("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()
|
||
|
||
# Draw arrow
|
||
cr.set_line_cap(cairo.LINE_CAP_SQUARE)
|
||
first_pt = None
|
||
i = 0
|
||
for segment, args in recording.value:
|
||
if segment == "moveTo":
|
||
first_pt = args[0]
|
||
continue
|
||
if first_pt is None:
|
||
continue
|
||
second_pt = args[0]
|
||
|
||
if idx is None or i == idx:
|
||
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,
|
||
)
|
||
)
|
||
cr.scale(1 / scale, 1 / scale)
|
||
cr.translate(self.start_handle_width, 0)
|
||
cr.move_to(0, 0)
|
||
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()
|
||
|
||
def draw_cupcake(self):
|
||
self.set_size(self.width, self.height)
|
||
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(
|
||
"@cairo: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(
|
||
"@cairo: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)
|
||
|
||
|
||
class InterpolatablePostscriptLike(InterpolatablePlot):
|
||
@wraps(InterpolatablePlot.__init__)
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
|
||
def __exit__(self, type, value, traceback):
|
||
self.surface.finish()
|
||
|
||
def set_size(self, width, height):
|
||
self.surface.set_size(width, height)
|
||
|
||
def show_page(self):
|
||
self.surface.show_page()
|
||
|
||
def __enter__(self):
|
||
self.surface = cairo.PSSurface(self.out, self.width, self.height)
|
||
return self
|
||
|
||
|
||
class InterpolatablePS(InterpolatablePostscriptLike):
|
||
def __enter__(self):
|
||
self.surface = cairo.PSSurface(self.out, self.width, self.height)
|
||
return self
|
||
|
||
|
||
class InterpolatablePDF(InterpolatablePostscriptLike):
|
||
def __enter__(self):
|
||
self.surface = cairo.PDFSurface(self.out, self.width, self.height)
|
||
self.surface.set_metadata(
|
||
cairo.PDF_METADATA_CREATOR, "fonttools varLib.interpolatable"
|
||
)
|
||
self.surface.set_metadata(cairo.PDF_METADATA_CREATE_DATE, "")
|
||
return self
|
||
|
||
|
||
class InterpolatableSVG(InterpolatablePlot):
|
||
@wraps(InterpolatablePlot.__init__)
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
|
||
def __enter__(self):
|
||
self.surface = None
|
||
return self
|
||
|
||
def __exit__(self, type, value, traceback):
|
||
if self.surface is not None:
|
||
self.show_page()
|
||
|
||
def set_size(self, width, height):
|
||
self.sink = BytesIO()
|
||
self.surface = cairo.SVGSurface(self.sink, width, height)
|
||
|
||
def show_page(self):
|
||
self.surface.finish()
|
||
self.out.append(self.sink.getvalue())
|
||
self.surface = None
|