interpolatablePlot.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699
  1. from fontTools.pens.recordingPen import (
  2. RecordingPen,
  3. DecomposingRecordingPen,
  4. RecordingPointPen,
  5. )
  6. from fontTools.pens.boundsPen import ControlBoundsPen
  7. from fontTools.pens.cairoPen import CairoPen
  8. from fontTools.pens.pointPen import (
  9. SegmentToPointPen,
  10. PointToSegmentPen,
  11. ReverseContourPointPen,
  12. )
  13. from fontTools.varLib.interpolatable import (
  14. PerContourOrComponentPen,
  15. SimpleRecordingPointPen,
  16. )
  17. from itertools import cycle
  18. from functools import wraps
  19. from io import BytesIO
  20. import cairo
  21. import math
  22. import logging
  23. log = logging.getLogger("fontTools.varLib.interpolatable")
  24. class LerpGlyphSet:
  25. def __init__(self, glyphset1, glyphset2, factor=0.5):
  26. self.glyphset1 = glyphset1
  27. self.glyphset2 = glyphset2
  28. self.factor = factor
  29. def __getitem__(self, glyphname):
  30. return LerpGlyph(glyphname, self)
  31. class LerpGlyph:
  32. def __init__(self, glyphname, glyphset):
  33. self.glyphset = glyphset
  34. self.glyphname = glyphname
  35. def draw(self, pen):
  36. recording1 = DecomposingRecordingPen(self.glyphset.glyphset1)
  37. self.glyphset.glyphset1[self.glyphname].draw(recording1)
  38. recording2 = DecomposingRecordingPen(self.glyphset.glyphset2)
  39. self.glyphset.glyphset2[self.glyphname].draw(recording2)
  40. factor = self.glyphset.factor
  41. for (op1, args1), (op2, args2) in zip(recording1.value, recording2.value):
  42. if op1 != op2:
  43. raise ValueError("Mismatching operations: %s, %s" % (op1, op2))
  44. mid_args = [
  45. (x1 + (x2 - x1) * factor, y1 + (y2 - y1) * factor)
  46. for (x1, y1), (x2, y2) in zip(args1, args2)
  47. ]
  48. getattr(pen, op1)(*mid_args)
  49. class OverridingDict(dict):
  50. def __init__(self, parent_dict):
  51. self.parent_dict = parent_dict
  52. def __missing__(self, key):
  53. return self.parent_dict[key]
  54. class InterpolatablePlot:
  55. width = 640
  56. height = 480
  57. pad = 16
  58. line_height = 36
  59. head_color = (0.3, 0.3, 0.3)
  60. label_color = (0.2, 0.2, 0.2)
  61. border_color = (0.9, 0.9, 0.9)
  62. border_width = 1
  63. fill_color = (0.8, 0.8, 0.8)
  64. stroke_color = (0.1, 0.1, 0.1)
  65. stroke_width = 2
  66. oncurve_node_color = (0, 0.8, 0)
  67. oncurve_node_diameter = 10
  68. offcurve_node_color = (0, 0.5, 0)
  69. offcurve_node_diameter = 8
  70. handle_color = (0.2, 1, 0.2)
  71. handle_width = 1
  72. other_start_point_color = (0, 0, 1)
  73. reversed_start_point_color = (0, 1, 0)
  74. start_point_color = (1, 0, 0)
  75. start_point_width = 15
  76. start_handle_width = 5
  77. start_handle_length = 100
  78. start_handle_arrow_length = 5
  79. contour_colors = ((1, 0, 0), (0, 0, 1), (0, 1, 0), (1, 1, 0), (1, 0, 1), (0, 1, 1))
  80. contour_alpha = 0.5
  81. cupcake_color = (0.3, 0, 0.3)
  82. cupcake = r"""
  83. ,@.
  84. ,@.@@,.
  85. ,@@,.@@@. @.@@@,.
  86. ,@@. @@@. @@. @@,.
  87. ,@@@.@,.@. @. @@@@,.@.@@,.
  88. ,@@.@. @@.@@. @,. .@' @' @@,
  89. ,@@. @. .@@.@@@. @@' @,
  90. ,@. @@. @,
  91. @. @,@@,. , .@@,
  92. @,. .@,@@,. .@@,. , .@@, @, @,
  93. @. .@. @ @@,. , @
  94. @,.@@. @,. @@,. @. @,. @'
  95. @@||@,. @'@,. @@,. @@ @,. @'@@, @'
  96. \\@@@@' @,. @'@@@@' @@,. @@@' //@@@'
  97. |||||||| @@,. @@' ||||||| |@@@|@|| ||
  98. \\\\\\\ ||@@@|| ||||||| ||||||| //
  99. ||||||| |||||| |||||| |||||| ||
  100. \\\\\\ |||||| |||||| |||||| //
  101. |||||| ||||| ||||| ||||| ||
  102. \\\\\ ||||| ||||| ||||| //
  103. ||||| |||| ||||| |||| ||
  104. \\\\ |||| |||| |||| //
  105. ||||||||||||||||||||||||
  106. """
  107. shrug_color = (0, 0.3, 0.3)
  108. shrug = r"""\_(")_/"""
  109. def __init__(self, out, glyphsets, names=None, **kwargs):
  110. self.out = out
  111. self.glyphsets = glyphsets
  112. self.names = names or [repr(g) for g in glyphsets]
  113. for k, v in kwargs.items():
  114. if not hasattr(self, k):
  115. raise TypeError("Unknown keyword argument: %s" % k)
  116. setattr(self, k, v)
  117. def __enter__(self):
  118. return self
  119. def __exit__(self, type, value, traceback):
  120. pass
  121. def set_size(self, width, height):
  122. raise NotImplementedError
  123. def show_page(self):
  124. raise NotImplementedError
  125. def add_problems(self, problems):
  126. for glyph, glyph_problems in problems.items():
  127. last_masters = None
  128. current_glyph_problems = []
  129. for p in glyph_problems:
  130. masters = (
  131. p["master_idx"]
  132. if "master_idx" in p
  133. else (p["master_1_idx"], p["master_2_idx"])
  134. )
  135. if masters == last_masters:
  136. current_glyph_problems.append(p)
  137. continue
  138. # Flush
  139. if current_glyph_problems:
  140. self.add_problem(glyph, current_glyph_problems)
  141. self.show_page()
  142. current_glyph_problems = []
  143. last_masters = masters
  144. current_glyph_problems.append(p)
  145. if current_glyph_problems:
  146. self.add_problem(glyph, current_glyph_problems)
  147. self.show_page()
  148. def add_problem(self, glyphname, problems):
  149. if type(problems) not in (list, tuple):
  150. problems = [problems]
  151. problem_type = problems[0]["type"]
  152. problem_types = set(problem["type"] for problem in problems)
  153. if not all(pt == problem_type for pt in problem_types):
  154. problem_type = ", ".join(sorted({problem["type"] for problem in problems}))
  155. log.info("Drawing %s: %s", glyphname, problem_type)
  156. master_keys = (
  157. ("master_idx",)
  158. if "master_idx" in problems[0]
  159. else ("master_1_idx", "master_2_idx")
  160. )
  161. master_indices = [problems[0][k] for k in master_keys]
  162. if problem_type == "missing":
  163. sample_glyph = next(
  164. i for i, m in enumerate(self.glyphsets) if m[glyphname] is not None
  165. )
  166. master_indices.insert(0, sample_glyph)
  167. total_width = self.width * 2 + 3 * self.pad
  168. total_height = (
  169. self.pad
  170. + self.line_height
  171. + self.pad
  172. + len(master_indices) * (self.height + self.pad * 2 + self.line_height)
  173. + self.pad
  174. )
  175. self.set_size(total_width, total_height)
  176. x = self.pad
  177. y = self.pad
  178. self.draw_label(glyphname, x=x, y=y, color=self.head_color, align=0, bold=True)
  179. self.draw_label(
  180. problem_type,
  181. x=x + self.width + self.pad,
  182. y=y,
  183. color=self.head_color,
  184. align=1,
  185. bold=True,
  186. )
  187. y += self.line_height + self.pad
  188. for which, master_idx in enumerate(master_indices):
  189. glyphset = self.glyphsets[master_idx]
  190. name = self.names[master_idx]
  191. self.draw_label(name, x=x, y=y, color=self.label_color, align=0.5)
  192. y += self.line_height + self.pad
  193. if glyphset[glyphname] is not None:
  194. self.draw_glyph(glyphset, glyphname, problems, which, x=x, y=y)
  195. else:
  196. self.draw_shrug(x=x, y=y)
  197. y += self.height + self.pad
  198. if any(
  199. pt in ("nothing", "wrong_start_point", "contour_order", "wrong_structure")
  200. for pt in problem_types
  201. ):
  202. x = self.pad + self.width + self.pad
  203. y = self.pad
  204. y += self.line_height + self.pad
  205. glyphset1 = self.glyphsets[master_indices[0]]
  206. glyphset2 = self.glyphsets[master_indices[1]]
  207. # Draw the mid-way of the two masters
  208. self.draw_label(
  209. "midway interpolation", x=x, y=y, color=self.head_color, align=0.5
  210. )
  211. y += self.line_height + self.pad
  212. midway_glyphset = LerpGlyphSet(glyphset1, glyphset2)
  213. self.draw_glyph(
  214. midway_glyphset, glyphname, {"type": "midway"}, None, x=x, y=y
  215. )
  216. y += self.height + self.pad
  217. # Draw the fixed mid-way of the two masters
  218. self.draw_label("proposed fix", x=x, y=y, color=self.head_color, align=0.5)
  219. y += self.line_height + self.pad
  220. if problem_type == "wrong_structure":
  221. self.draw_shrug(x=x, y=y)
  222. return
  223. overriding1 = OverridingDict(glyphset1)
  224. overriding2 = OverridingDict(glyphset2)
  225. perContourPen1 = PerContourOrComponentPen(
  226. RecordingPen, glyphset=overriding1
  227. )
  228. perContourPen2 = PerContourOrComponentPen(
  229. RecordingPen, glyphset=overriding2
  230. )
  231. glyphset1[glyphname].draw(perContourPen1)
  232. glyphset2[glyphname].draw(perContourPen2)
  233. for problem in problems:
  234. if problem["type"] == "contour_order":
  235. fixed_contours = [
  236. perContourPen2.value[i] for i in problems[0]["value_2"]
  237. ]
  238. perContourPen2.value = fixed_contours
  239. for problem in problems:
  240. if problem["type"] == "wrong_start_point":
  241. # Save the wrong contours
  242. wrongContour1 = perContourPen1.value[problem["contour"]]
  243. wrongContour2 = perContourPen2.value[problem["contour"]]
  244. # Convert the wrong contours to point pens
  245. points1 = RecordingPointPen()
  246. converter = SegmentToPointPen(points1, False)
  247. wrongContour1.replay(converter)
  248. points2 = RecordingPointPen()
  249. converter = SegmentToPointPen(points2, False)
  250. wrongContour2.replay(converter)
  251. proposed_start = problem["value_2"]
  252. # See if we need reversing; fragile but worth a try
  253. if problem["reversed"]:
  254. new_points2 = RecordingPointPen()
  255. reversedPen = ReverseContourPointPen(new_points2)
  256. points2.replay(reversedPen)
  257. points2 = new_points2
  258. proposed_start = len(points2.value) - 2 - proposed_start
  259. # Rotate points2 so that the first point is the same as in points1
  260. beginPath = points2.value[:1]
  261. endPath = points2.value[-1:]
  262. pts = points2.value[1:-1]
  263. pts = pts[proposed_start:] + pts[:proposed_start]
  264. points2.value = beginPath + pts + endPath
  265. # Convert the point pens back to segment pens
  266. segment1 = RecordingPen()
  267. converter = PointToSegmentPen(segment1, True)
  268. points1.replay(converter)
  269. segment2 = RecordingPen()
  270. converter = PointToSegmentPen(segment2, True)
  271. points2.replay(converter)
  272. # Replace the wrong contours
  273. wrongContour1.value = segment1.value
  274. wrongContour2.value = segment2.value
  275. # Assemble
  276. fixed1 = RecordingPen()
  277. fixed2 = RecordingPen()
  278. for contour in perContourPen1.value:
  279. fixed1.value.extend(contour.value)
  280. for contour in perContourPen2.value:
  281. fixed2.value.extend(contour.value)
  282. fixed1.draw = fixed1.replay
  283. fixed2.draw = fixed2.replay
  284. overriding1[glyphname] = fixed1
  285. overriding2[glyphname] = fixed2
  286. try:
  287. midway_glyphset = LerpGlyphSet(overriding1, overriding2)
  288. self.draw_glyph(
  289. midway_glyphset, glyphname, {"type": "fixed"}, None, x=x, y=y
  290. )
  291. except ValueError:
  292. self.draw_shrug(x=x, y=y)
  293. y += self.height + self.pad
  294. def draw_label(self, label, *, x, y, color=(0, 0, 0), align=0, bold=False):
  295. cr = cairo.Context(self.surface)
  296. cr.select_font_face(
  297. "@cairo:",
  298. cairo.FONT_SLANT_NORMAL,
  299. cairo.FONT_WEIGHT_BOLD if bold else cairo.FONT_WEIGHT_NORMAL,
  300. )
  301. cr.set_font_size(self.line_height)
  302. font_extents = cr.font_extents()
  303. font_size = self.line_height * self.line_height / font_extents[2]
  304. cr.set_font_size(font_size)
  305. font_extents = cr.font_extents()
  306. cr.set_source_rgb(*color)
  307. extents = cr.text_extents(label)
  308. if extents.width > self.width:
  309. # Shrink
  310. font_size *= self.width / extents.width
  311. cr.set_font_size(font_size)
  312. font_extents = cr.font_extents()
  313. extents = cr.text_extents(label)
  314. # Center
  315. label_x = x + (self.width - extents.width) * align
  316. label_y = y + font_extents[0]
  317. cr.move_to(label_x, label_y)
  318. cr.show_text(label)
  319. def draw_glyph(self, glyphset, glyphname, problems, which, *, x=0, y=0):
  320. if type(problems) not in (list, tuple):
  321. problems = [problems]
  322. problem_type = problems[0]["type"]
  323. problem_types = set(problem["type"] for problem in problems)
  324. if not all(pt == problem_type for pt in problem_types):
  325. problem_type = "mixed"
  326. glyph = glyphset[glyphname]
  327. recording = RecordingPen()
  328. glyph.draw(recording)
  329. boundsPen = ControlBoundsPen(glyphset)
  330. recording.replay(boundsPen)
  331. glyph_width = boundsPen.bounds[2] - boundsPen.bounds[0]
  332. glyph_height = boundsPen.bounds[3] - boundsPen.bounds[1]
  333. scale = None
  334. if glyph_width:
  335. scale = self.width / glyph_width
  336. if glyph_height:
  337. if scale is None:
  338. scale = self.height / glyph_height
  339. else:
  340. scale = min(scale, self.height / glyph_height)
  341. if scale is None:
  342. scale = 1
  343. cr = cairo.Context(self.surface)
  344. cr.translate(x, y)
  345. # Center
  346. cr.translate(
  347. (self.width - glyph_width * scale) / 2,
  348. (self.height - glyph_height * scale) / 2,
  349. )
  350. cr.scale(scale, -scale)
  351. cr.translate(-boundsPen.bounds[0], -boundsPen.bounds[3])
  352. if self.border_color:
  353. cr.set_source_rgb(*self.border_color)
  354. cr.rectangle(
  355. boundsPen.bounds[0], boundsPen.bounds[1], glyph_width, glyph_height
  356. )
  357. cr.set_line_width(self.border_width / scale)
  358. cr.stroke()
  359. if self.fill_color and problem_type != "open_path":
  360. pen = CairoPen(glyphset, cr)
  361. recording.replay(pen)
  362. cr.set_source_rgb(*self.fill_color)
  363. cr.fill()
  364. if self.stroke_color:
  365. pen = CairoPen(glyphset, cr)
  366. recording.replay(pen)
  367. cr.set_source_rgb(*self.stroke_color)
  368. cr.set_line_width(self.stroke_width / scale)
  369. cr.stroke()
  370. if problem_type in (
  371. "nothing",
  372. "node_count",
  373. "node_incompatibility",
  374. "wrong_structure",
  375. ):
  376. cr.set_line_cap(cairo.LINE_CAP_ROUND)
  377. # Oncurve nodes
  378. for segment, args in recording.value:
  379. if not args:
  380. continue
  381. x, y = args[-1]
  382. cr.move_to(x, y)
  383. cr.line_to(x, y)
  384. cr.set_source_rgb(*self.oncurve_node_color)
  385. cr.set_line_width(self.oncurve_node_diameter / scale)
  386. cr.stroke()
  387. # Offcurve nodes
  388. for segment, args in recording.value:
  389. for x, y in args[:-1]:
  390. cr.move_to(x, y)
  391. cr.line_to(x, y)
  392. cr.set_source_rgb(*self.offcurve_node_color)
  393. cr.set_line_width(self.offcurve_node_diameter / scale)
  394. cr.stroke()
  395. # Handles
  396. for segment, args in recording.value:
  397. if not args:
  398. pass
  399. elif segment in ("moveTo", "lineTo"):
  400. cr.move_to(*args[0])
  401. elif segment == "qCurveTo":
  402. for x, y in args:
  403. cr.line_to(x, y)
  404. cr.new_sub_path()
  405. cr.move_to(*args[-1])
  406. elif segment == "curveTo":
  407. cr.line_to(*args[0])
  408. cr.new_sub_path()
  409. cr.move_to(*args[1])
  410. cr.line_to(*args[2])
  411. cr.new_sub_path()
  412. cr.move_to(*args[-1])
  413. else:
  414. assert False
  415. cr.set_source_rgb(*self.handle_color)
  416. cr.set_line_width(self.handle_width / scale)
  417. cr.stroke()
  418. matching = None
  419. for problem in problems:
  420. if problem["type"] == "contour_order":
  421. matching = problem["value_2"]
  422. colors = cycle(self.contour_colors)
  423. perContourPen = PerContourOrComponentPen(
  424. RecordingPen, glyphset=glyphset
  425. )
  426. recording.replay(perContourPen)
  427. for i, contour in enumerate(perContourPen.value):
  428. if matching[i] == i:
  429. continue
  430. color = next(colors)
  431. contour.replay(CairoPen(glyphset, cr))
  432. cr.set_source_rgba(*color, self.contour_alpha)
  433. cr.fill()
  434. for problem in problems:
  435. if problem["type"] in ("nothing", "wrong_start_point", "wrong_structure"):
  436. idx = problem.get("contour")
  437. # Draw suggested point
  438. if idx is not None and which == 1 and "value_2" in problem:
  439. perContourPen = PerContourOrComponentPen(
  440. RecordingPen, glyphset=glyphset
  441. )
  442. recording.replay(perContourPen)
  443. points = SimpleRecordingPointPen()
  444. converter = SegmentToPointPen(points, False)
  445. perContourPen.value[
  446. idx if matching is None else matching[idx]
  447. ].replay(converter)
  448. targetPoint = points.value[problem["value_2"]][0]
  449. cr.move_to(*targetPoint)
  450. cr.line_to(*targetPoint)
  451. cr.set_line_cap(cairo.LINE_CAP_ROUND)
  452. cr.set_source_rgb(*self.other_start_point_color)
  453. cr.set_line_width(self.start_point_width / scale)
  454. cr.stroke()
  455. # Draw start point
  456. cr.set_line_cap(cairo.LINE_CAP_ROUND)
  457. i = 0
  458. for segment, args in recording.value:
  459. if segment == "moveTo":
  460. if idx is None or i == idx:
  461. cr.move_to(*args[0])
  462. cr.line_to(*args[0])
  463. i += 1
  464. if which == 0 or not problem.get("reversed"):
  465. cr.set_source_rgb(*self.start_point_color)
  466. else:
  467. cr.set_source_rgb(*self.reversed_start_point_color)
  468. cr.set_line_width(self.start_point_width / scale)
  469. cr.stroke()
  470. # Draw arrow
  471. cr.set_line_cap(cairo.LINE_CAP_SQUARE)
  472. first_pt = None
  473. i = 0
  474. for segment, args in recording.value:
  475. if segment == "moveTo":
  476. first_pt = args[0]
  477. continue
  478. if first_pt is None:
  479. continue
  480. second_pt = args[0]
  481. if idx is None or i == idx:
  482. first_pt = complex(*first_pt)
  483. second_pt = complex(*second_pt)
  484. length = abs(second_pt - first_pt)
  485. if length:
  486. # Draw handle
  487. length *= scale
  488. second_pt = (
  489. first_pt
  490. + (second_pt - first_pt)
  491. / length
  492. * self.start_handle_length
  493. )
  494. cr.move_to(first_pt.real, first_pt.imag)
  495. cr.line_to(second_pt.real, second_pt.imag)
  496. # Draw arrowhead
  497. cr.save()
  498. cr.translate(second_pt.real, second_pt.imag)
  499. cr.rotate(
  500. math.atan2(
  501. second_pt.imag - first_pt.imag,
  502. second_pt.real - first_pt.real,
  503. )
  504. )
  505. cr.scale(1 / scale, 1 / scale)
  506. cr.translate(self.start_handle_width, 0)
  507. cr.move_to(0, 0)
  508. cr.line_to(
  509. -self.start_handle_arrow_length,
  510. -self.start_handle_arrow_length,
  511. )
  512. cr.line_to(
  513. -self.start_handle_arrow_length,
  514. self.start_handle_arrow_length,
  515. )
  516. cr.close_path()
  517. cr.restore()
  518. first_pt = None
  519. i += 1
  520. cr.set_line_width(self.start_handle_width / scale)
  521. cr.stroke()
  522. def draw_cupcake(self):
  523. self.set_size(self.width, self.height)
  524. cupcake = self.cupcake.splitlines()
  525. cr = cairo.Context(self.surface)
  526. cr.set_source_rgb(*self.cupcake_color)
  527. cr.set_font_size(self.line_height)
  528. cr.select_font_face(
  529. "@cairo:monospace", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL
  530. )
  531. width = 0
  532. height = 0
  533. for line in cupcake:
  534. extents = cr.text_extents(line)
  535. width = max(width, extents.width)
  536. height += extents.height
  537. if not width:
  538. return
  539. cr.scale(self.width / width, self.height / height)
  540. for line in cupcake:
  541. cr.translate(0, cr.text_extents(line).height)
  542. cr.move_to(0, 0)
  543. cr.show_text(line)
  544. def draw_shrug(self, x=0, y=0):
  545. cr = cairo.Context(self.surface)
  546. cr.translate(x, y)
  547. cr.set_source_rgb(*self.shrug_color)
  548. cr.set_font_size(self.line_height)
  549. cr.select_font_face(
  550. "@cairo:monospace", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL
  551. )
  552. extents = cr.text_extents(self.shrug)
  553. if not extents.width:
  554. return
  555. cr.translate(0, self.height * 0.6)
  556. scale = self.width / extents.width
  557. cr.scale(scale, scale)
  558. cr.move_to(-extents.x_bearing, 0)
  559. cr.show_text(self.shrug)
  560. class InterpolatablePostscriptLike(InterpolatablePlot):
  561. @wraps(InterpolatablePlot.__init__)
  562. def __init__(self, *args, **kwargs):
  563. super().__init__(*args, **kwargs)
  564. def __exit__(self, type, value, traceback):
  565. self.surface.finish()
  566. def set_size(self, width, height):
  567. self.surface.set_size(width, height)
  568. def show_page(self):
  569. self.surface.show_page()
  570. def __enter__(self):
  571. self.surface = cairo.PSSurface(self.out, self.width, self.height)
  572. return self
  573. class InterpolatablePS(InterpolatablePostscriptLike):
  574. def __enter__(self):
  575. self.surface = cairo.PSSurface(self.out, self.width, self.height)
  576. return self
  577. class InterpolatablePDF(InterpolatablePostscriptLike):
  578. def __enter__(self):
  579. self.surface = cairo.PDFSurface(self.out, self.width, self.height)
  580. self.surface.set_metadata(
  581. cairo.PDF_METADATA_CREATOR, "fonttools varLib.interpolatable"
  582. )
  583. self.surface.set_metadata(cairo.PDF_METADATA_CREATE_DATE, "")
  584. return self
  585. class InterpolatableSVG(InterpolatablePlot):
  586. @wraps(InterpolatablePlot.__init__)
  587. def __init__(self, *args, **kwargs):
  588. super().__init__(*args, **kwargs)
  589. def __enter__(self):
  590. self.surface = None
  591. return self
  592. def __exit__(self, type, value, traceback):
  593. if self.surface is not None:
  594. self.show_page()
  595. def set_size(self, width, height):
  596. self.sink = BytesIO()
  597. self.surface = cairo.SVGSurface(self.sink, width, height)
  598. def show_page(self):
  599. self.surface.finish()
  600. self.out.append(self.sink.getvalue())
  601. self.surface = None