Skip to content

Instantly share code, notes, and snippets.

@celestialphineas
Created December 1, 2020 03:22
Show Gist options
  • Save celestialphineas/06ebaaba4b689dd70704950ace3ee984 to your computer and use it in GitHub Desktop.
Save celestialphineas/06ebaaba4b689dd70704950ace3ee984 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
# -*- encoding: utf8 -*-
import cairocffi as cairo
import numpy as np
# Adding the implicit on-points back to the contour
# The input is one single contour, not all of the glyph's contours
def regularize_quadratic_contour(contour):
result = []
if not contour[0]['on']:
if contour[-1]['on']:
result.append({ 'x': float(contour[-1]['x']),
'y': float(contour[-1]['y']), 'on': True })
else:
first_x = contour[0]['x']; first_y = contour[0]['y']
last_x = contour[-1]['x']; last_y = contour[-1]['y']
result.append({ 'x': (first_x + last_x)/2,
'y': (first_y + last_y)/2, 'on': True })
for i, pt in enumerate(contour[:-1]):
result.append({ 'x': float(pt['x']), 'y': float(pt['y']), 'on': pt['on'] })
next_pt = contour[i+1]
if not pt['on'] and not next_pt['on']:
result.append({ 'x': (pt['x'] + next_pt['x'])/2,
'y': (pt['y'] + next_pt['y'])/2, 'on': True })
# Do nothing when the last point is moved to the head
if contour[-1]['on'] and not contour[0]['on']: pass
else: result.append({ 'x': float(contour[-1]['x']),
'y': float(contour[-1]['y']), 'on': contour[-1]['on'] })
# Making the top-rightest on-point as the starting point
max_on_i = 0; max_x = -float('inf'); max_y = -float('inf')
for i, pt in enumerate(result):
if pt['on']:
if pt['y'] > max_y:
max_on_i = i; max_x = pt['x']; max_y = pt['y']
if pt['y'] == max_y and pt['x'] > max_x:
max_on_i = i; max_x = pt['x']
return [] if not result else (result[max_on_i:] + result[:max_on_i])
# Converting a quadratic Bezier segment to cubic
# Return a tuple of the control point coordinates
def quadratic2cubic(x0, y0, x1, y1, x2, y2):
cx1 = x0 + (x1-x0)*2/3; cy1 = y0 + (y1-y0)*2/3
cx2 = x2 + (x1-x2)*2/3; cy2 = y2 + (y1-y2)*2/3
return ( cx1, cy1, cx2, cy2 )
# A routine for rasterizing quadratic contours
def rasterize_quadratic(contours, upm=1000, size=(1000, 1000)):
"""A routine for rasterizing quadratic contours
Parameters
----------
contours: { "x": number, "y": number, "on": boolean }[][]
Representation of the glyph's contours
upm: number
Units per em-box, usually 1000, sometimes 256 or 1024
The value varies by fonts
size: (number, number)
Shape of the output image
"""
h, w = size
# Regularized contours
cs = [ regularize_quadratic_contour(c) for c in contours ]
# Helper function for coordinate transformation and unpacking
coord = lambda pt: (pt['x']/upm*w, pt['y']/upm*h)
# Setting up cairo surface
surface = cairo.ImageSurface(cairo.FORMAT_A8, w, h)
ctx = cairo.Context(surface)
ctx.set_fill_rule(cairo.FILL_RULE_WINDING)
ctx.set_source_rgb(1, 1, 1)
# Draw the contours
for c in cs:
ctx.move_to(*coord(c[0]))
i = 0
while i < len(c) - 1:
if not c[i]['on']: raise Exception('Invalid regularized quadratic.')
if not c[i+1]['on']:
end_pt = c[i+2] if i+2 < len(c) else c[0]
cps = quadratic2cubic(*coord(c[i]), *coord(c[i+1]), *coord(end_pt))
ctx.curve_to(*cps, *coord(end_pt))
i += 2
else:
end_pt = c[i+1] if i+1 < len(c) else c[0]
ctx.line_to(*coord(end_pt))
i += 1
ctx.fill()
buf = surface.get_data()
arr = np.frombuffer(buf, dtype=np.uint8).reshape(h, w).copy()
return arr
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment