Add Text shape
Rendered using freetype-py and matplotlib Can eliminate the matplotlib dependency if I write my own bezier code, but that's work (and I already use matplotlib...).
This commit is contained in:
parent
3e1ff19270
commit
3a460a9296
@ -11,9 +11,10 @@ E-beam doses, and the ability to output to multiple formats.
|
|||||||
Requirements:
|
Requirements:
|
||||||
* python 3 (written and tested with 3.5)
|
* python 3 (written and tested with 3.5)
|
||||||
* numpy
|
* numpy
|
||||||
* matplotlib (optional, used for visualization functions)
|
* matplotlib (optional, used for visualization functions and text)
|
||||||
* python-gdsii (optional, used for gdsii i/o)
|
* python-gdsii (optional, used for gdsii i/o)
|
||||||
* svgwrite (optional, used for svg output)
|
* svgwrite (optional, used for svg output)
|
||||||
|
* freetype (optional, used for text)
|
||||||
|
|
||||||
|
|
||||||
Install with pip, via git:
|
Install with pip, via git:
|
||||||
|
@ -9,4 +9,4 @@ from .polygon import Polygon
|
|||||||
from .circle import Circle
|
from .circle import Circle
|
||||||
from .ellipse import Ellipse
|
from .ellipse import Ellipse
|
||||||
from .arc import Arc
|
from .arc import Arc
|
||||||
|
from .text import Text
|
||||||
|
@ -1,57 +1,201 @@
|
|||||||
|
from typing import List, Tuple
|
||||||
|
import numpy
|
||||||
|
from numpy import pi, inf
|
||||||
|
|
||||||
#
|
from . import Shape, Polygon, normalized_shape_tuple
|
||||||
# class Text(Shape):
|
from .. import PatternError
|
||||||
# _string = ''
|
from ..utils import is_scalar, vector2, get_bit
|
||||||
# _height = 1.0
|
|
||||||
# _rotation = 0.0
|
# Loaded on use:
|
||||||
# font_path = ''
|
# from freetype import Face
|
||||||
#
|
# from matplotlib.path import Path
|
||||||
# # vertices property
|
|
||||||
# @property
|
|
||||||
# def string(self):
|
__author__ = 'Jan Petykiewicz'
|
||||||
# return self._string
|
|
||||||
#
|
|
||||||
# @string.setter
|
class Text(Shape):
|
||||||
# def string(self, val):
|
_string = ''
|
||||||
# self._string = val
|
_height = 1.0
|
||||||
#
|
_rotation = 0.0
|
||||||
# # Rotation property
|
font_path = ''
|
||||||
# @property
|
|
||||||
# def rotation(self):
|
# vertices property
|
||||||
# return self._rotation
|
@property
|
||||||
#
|
def string(self) -> str:
|
||||||
# @rotation.setter
|
return self._string
|
||||||
# def rotation(self, val):
|
|
||||||
# if not is_scalar(val):
|
@string.setter
|
||||||
# raise PatternError('Rotation must be a scalar')
|
def string(self, val: str):
|
||||||
# self._rotation = val % (2 * pi)
|
self._string = val
|
||||||
#
|
|
||||||
# # Height property
|
# Rotation property
|
||||||
# @property
|
@property
|
||||||
# def height(self):
|
def rotation(self) -> float:
|
||||||
# return self._height
|
return self._rotation
|
||||||
#
|
|
||||||
# @height.setter
|
@rotation.setter
|
||||||
# def height(self, val):
|
def rotation(self, val: float):
|
||||||
# if not is_scalar(val):
|
if not is_scalar(val):
|
||||||
# raise PatternError('Height must be a scalar')
|
raise PatternError('Rotation must be a scalar')
|
||||||
# self._height = val
|
self._rotation = val % (2 * pi)
|
||||||
#
|
|
||||||
# def __init__(self, text, height, font_path, rotation=0.0, offset=(0.0, 0.0), layer=0, dose=1.0):
|
# Height property
|
||||||
# self.offset = offset
|
@property
|
||||||
# self.layer = layer
|
def height(self) -> float:
|
||||||
# self.dose = dose
|
return self._height
|
||||||
# self.text = text
|
|
||||||
# self.height = height
|
@height.setter
|
||||||
# self.rotation = rotation
|
def height(self, val: float):
|
||||||
# self.font_path = font_path
|
if not is_scalar(val):
|
||||||
#
|
raise PatternError('Height must be a scalar')
|
||||||
# def to_polygon(self, _poly_num_points=None, _poly_max_arclen=None):
|
self._height = val
|
||||||
#
|
|
||||||
# return copy.deepcopy(self)
|
def __init__(self,
|
||||||
#
|
string: str,
|
||||||
# def rotate(self, theta):
|
height: float,
|
||||||
# self.rotation += theta
|
font_path: str,
|
||||||
#
|
rotation: float=0.0,
|
||||||
# def scale_by(self, c):
|
offset: vector2=(0.0, 0.0),
|
||||||
# self.height *= c
|
layer: int=0,
|
||||||
|
dose: float=1.0):
|
||||||
|
self.offset = offset
|
||||||
|
self.layer = layer
|
||||||
|
self.dose = dose
|
||||||
|
self.string = string
|
||||||
|
self.height = height
|
||||||
|
self.rotation = rotation
|
||||||
|
self.font_path = font_path
|
||||||
|
|
||||||
|
def to_polygons(self,
|
||||||
|
_poly_num_points: int=None,
|
||||||
|
_poly_max_arclen: float=None
|
||||||
|
) -> List[Polygon]:
|
||||||
|
all_polygons = []
|
||||||
|
total_advance = 0
|
||||||
|
for char in self.string:
|
||||||
|
raw_polys, advance = get_char_as_polygons(self.font_path, char)
|
||||||
|
|
||||||
|
# Move these polygons to the right of the previous letter
|
||||||
|
for xys in raw_polys:
|
||||||
|
poly = Polygon(xys, dose=self.dose, layer=self.layer)
|
||||||
|
poly.scale_by(self.height)
|
||||||
|
poly.offset = self.offset + [total_advance, 0]
|
||||||
|
# poly.scale_by(self.height)
|
||||||
|
poly.rotate_around(self.offset, self.rotation)
|
||||||
|
all_polygons += [poly]
|
||||||
|
|
||||||
|
# Update the list of all polygons and how far to advance
|
||||||
|
total_advance += advance * self.height
|
||||||
|
|
||||||
|
return all_polygons
|
||||||
|
|
||||||
|
def rotate(self, theta: float):
|
||||||
|
self.rotation += theta
|
||||||
|
|
||||||
|
def scale_by(self, c: float):
|
||||||
|
self.height *= c
|
||||||
|
|
||||||
|
def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
|
||||||
|
return (type(self), self.string, self.font_path, self.layer), \
|
||||||
|
(self.offset, self.height / norm_value, self.rotation, self.dose), \
|
||||||
|
lambda: Text(string=self.string,
|
||||||
|
height=self.height * norm_value,
|
||||||
|
font_path=self.font_path,
|
||||||
|
layer=self.layer)
|
||||||
|
|
||||||
|
def get_bounds(self) -> numpy.ndarray:
|
||||||
|
# rotation makes this a huge pain when using slot.advance and glyph.bbox(), so
|
||||||
|
# just convert to polygons instead
|
||||||
|
bounds = [[+inf, +inf], [-inf, -inf]]
|
||||||
|
polys = self.to_polygons()
|
||||||
|
for poly in polys:
|
||||||
|
poly_bounds = poly.get_bounds()
|
||||||
|
bounds[0, :] = numpy.minimum(bounds[0, :], poly_bounds[0, :])
|
||||||
|
bounds[1, :] = numpy.maximum(bounds[1, :], poly_bounds[1, :])
|
||||||
|
|
||||||
|
return bounds
|
||||||
|
|
||||||
|
|
||||||
|
def get_char_as_polygons(font_path: str,
|
||||||
|
char: str,
|
||||||
|
resolution: float=48*64,
|
||||||
|
) -> Tuple[List[List[List[float]]], float]:
|
||||||
|
from freetype import Face
|
||||||
|
from matplotlib.path import Path
|
||||||
|
|
||||||
|
"""
|
||||||
|
Get a list of polygons representing a single character.
|
||||||
|
|
||||||
|
The output is normalized so that the font size is 1 unit.
|
||||||
|
|
||||||
|
:param font_path: File path specifying a font loadable by freetype
|
||||||
|
:param char: Character to convert to polygons
|
||||||
|
:param resolution: Internal resolution setting (used for freetype
|
||||||
|
Face.set_font_size(resolution)). Modify at your own peril!
|
||||||
|
:return: List of polygons [[[x0, y0], [x1, y1], ...], ...] and 'advance' distance (distance
|
||||||
|
from the start of this glyph to the start of the next one)
|
||||||
|
"""
|
||||||
|
if len(char) != 1:
|
||||||
|
raise Exception('get_char_as_polygons called with non-char')
|
||||||
|
|
||||||
|
face = Face(font_path)
|
||||||
|
face.set_char_size(resolution)
|
||||||
|
face.load_char(char)
|
||||||
|
slot = face.glyph
|
||||||
|
outline = slot.outline
|
||||||
|
|
||||||
|
start = 0
|
||||||
|
all_verts, all_codes = [], []
|
||||||
|
for end in outline.contours:
|
||||||
|
points = outline.points[start:end + 1]
|
||||||
|
points.append(points[0])
|
||||||
|
|
||||||
|
tags = outline.tags[start:end + 1]
|
||||||
|
tags.append(tags[0])
|
||||||
|
|
||||||
|
segments = []
|
||||||
|
for j, point in enumerate(points):
|
||||||
|
# If we already have a segment, add this point to it
|
||||||
|
if j > 0:
|
||||||
|
segments[-1].append(point)
|
||||||
|
|
||||||
|
# If not bezier control point, start next segment
|
||||||
|
if get_bit(tags[j], 0) and j < (len(points) - 1):
|
||||||
|
segments.append([point])
|
||||||
|
|
||||||
|
verts = [points[0]]
|
||||||
|
codes = [Path.MOVETO]
|
||||||
|
for segment in segments:
|
||||||
|
if len(segment) == 2:
|
||||||
|
verts.extend(segment[1:])
|
||||||
|
codes.extend([Path.LINETO])
|
||||||
|
elif len(segment) == 3:
|
||||||
|
verts.extend(segment[1:])
|
||||||
|
codes.extend([Path.CURVE3, Path.CURVE3])
|
||||||
|
else:
|
||||||
|
verts.append(segment[1])
|
||||||
|
codes.append(Path.CURVE3)
|
||||||
|
for i in range(1, len(segment) - 2):
|
||||||
|
a, b = segment[i], segment[i + 1]
|
||||||
|
c = ((a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0)
|
||||||
|
verts.extend([c, b])
|
||||||
|
codes.extend([Path.CURVE3, Path.CURVE3])
|
||||||
|
verts.append(segment[-1])
|
||||||
|
codes.append(Path.CURVE3)
|
||||||
|
all_verts.extend(verts)
|
||||||
|
all_codes.extend(codes)
|
||||||
|
start = end + 1
|
||||||
|
|
||||||
|
all_verts = numpy.array(all_verts) / resolution
|
||||||
|
|
||||||
|
advance = slot.advance.x / resolution
|
||||||
|
|
||||||
|
if len(all_verts) == 0:
|
||||||
|
polygons = []
|
||||||
|
else:
|
||||||
|
path = Path(all_verts, all_codes)
|
||||||
|
path.should_simplify = False
|
||||||
|
polygons = path.to_polygons()
|
||||||
|
|
||||||
|
return polygons, advance
|
||||||
|
Loading…
Reference in New Issue
Block a user