def getControlBounds(drawable: Drawable, layer: GlyphSet | None) -> BoundingBox | None: pen = ControlBoundsPen(layer) # raise 'KeyError' when a referenced component is missing from glyph set pen.skipMissingComponents = False drawable.draw(pen) return None if pen.bounds is None else BoundingBox(*pen.bounds)
def getControlBounds(drawable: Drawable, layer: Any) -> Optional[BoundingBox]: # XXX: layer should behave like a mapping of glyph names to Glyph objects, but # cyclic imports... pen = ControlBoundsPen(layer) # raise 'KeyError' when a referenced component is missing from glyph set pen.skipMissingComponents = False drawable.draw(pen) return None if pen.bounds is None else BoundingBox(*pen.bounds)
def setupTable_hmtx(self): """ Make the hmtx table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ self.otf["hmtx"] = hmtx = newTable("hmtx") hmtx.metrics = {} for glyphName, glyph in self.allGlyphs.items(): width = glyph.width if width < 0: raise ValueError("The width should not be negative: '%s'" % (glyphName)) left = 0 if len(glyph) or len(glyph.components): # lsb should be consistent with glyf xMin, which is just # minimum x for coordinate data pen = ControlBoundsPen(self.ufo, ignoreSinglePoints=True) glyph.draw(pen) left = 0 if pen.bounds is None else pen.bounds[0] # take floor of lsb/xMin, as fontTools does with min bounds hmtx[glyphName] = (round(width), int(math.floor(left)))
def _get_controlPointBounds(self): from fontTools.pens.boundsPen import ControlBoundsPen if self._controlPointBoundsCache is None: pen = ControlBoundsPen(None) self.draw(pen) self._controlPointBoundsCache = pen.bounds return self._controlPointBoundsCache
def makeGlyphsBoundingBoxes(self): """ Make bounding boxes for all the glyphs, and return a dictionary of BoundingBox(xMin, xMax, yMin, yMax) namedtuples keyed by glyph names. The bounding box of empty glyphs (without contours or components) is set to None. Float values are rounded to integers using the built-in round(). **This should not be called externally.** Subclasses may override this method to handle the bounds creation in a different way if desired. """ def getControlPointBounds(glyph): pen.init() glyph.draw(pen) return pen.bounds glyphBoxes = {} pen = ControlBoundsPen(self.allGlyphs) for glyphName, glyph in self.allGlyphs.items(): bounds = None if glyph or glyph.components: bounds = getControlPointBounds(glyph) if bounds: bounds = BoundingBox(*(round(v) for v in bounds)) glyphBoxes[glyphName] = bounds return glyphBoxes
def setupTable_vmtx(self): """ Make the vmtx table. **This should not be called externally.** Subclasses may override or supplement this method to handle the table creation in a different way if desired. """ self.otf["vmtx"] = vmtx = newTable("vmtx") vmtx.metrics = {} for glyphName, glyph in self.allGlyphs.items(): height = glyph.height verticalOrigin = _getVerticalOrigin(glyph) top = 0 if len(glyph) or len(glyph.components): # tsb should be consistent with glyf yMax, which is just # maximum y for coordinate data pen = ControlBoundsPen(self.ufo, ignoreSinglePoints=True) glyph.draw(pen) if pen.bounds is not None: top = pen.bounds[3] # take ceil of tsb/yMax, as fontTools does with max bounds vmtx[glyphName] = (_roundInt(height), int(math.ceil(verticalOrigin) - math.ceil(top)))
def _transformed_glyph_bounds( ufo: ufoLib2.Font, glyph_name: str, transform: Affine2D) -> Optional[Tuple[float, float, float, float]]: glyph = ufo[glyph_name] pen = bounds_pen = ControlBoundsPen(ufo) if not transform.almost_equals(Affine2D.identity()): pen = TransformPen(bounds_pen, transform) glyph.draw(pen) return bounds_pen.bounds
def glyph(self, glyph_name): if isinstance(self.wrapped, fontTools.ttLib.TTFont): return self.glyphset[glyph_name] elif isinstance(self.wrapped, fontTools.t1Lib.T1Font): glyph = self.glyphset[glyph_name] if not hasattr(glyph, "width"): glyph.draw(ControlBoundsPen(self.glyphset)) glyph.lsb = 0 return glyph
def extractOpenTypeGlyphs(source, destination): # grab the cmap cmap = source["cmap"] vmtx = source.get("vmtx") vorg = source.get("VORG") preferred = [(3, 10, 12), (3, 10, 4), (3, 1, 12), (3, 1, 4), (0, 3, 12), (0, 3, 4), (3, 0, 12), (3, 0, 4), (1, 0, 12), (1, 0, 4)] found = {} for table in cmap.tables: found[table.platformID, table.platEncID, table.format] = table table = None for key in preferred: if key not in found: continue table = found[key] break reversedMapping = {} if table is not None: for uniValue, glyphName in table.cmap.items(): reversedMapping[glyphName] = uniValue # grab the glyphs glyphSet = source.getGlyphSet() for glyphName in glyphSet.keys(): sourceGlyph = glyphSet[glyphName] # make the new glyph destination.newGlyph(glyphName) destinationGlyph = destination[glyphName] # outlines pen = destinationGlyph.getPen() sourceGlyph.draw(pen) # width destinationGlyph.width = sourceGlyph.width # height and vertical origin if vmtx is not None and glyphName in vmtx.metrics: destinationGlyph.height = vmtx[glyphName][0] if vorg is not None: if glyphName in vorg.VOriginRecords: destinationGlyph.verticalOrigin = vorg[glyphName] else: destinationGlyph.verticalOrigin = vorg.defaultVertOriginY else: tsb = vmtx[glyphName][1] bounds_pen = ControlBoundsPen(glyphSet) sourceGlyph.draw(bounds_pen) if bounds_pen.bounds is None: continue xMin, yMin, xMax, yMax = bounds_pen.bounds destinationGlyph.verticalOrigin = tsb + yMax # unicode destinationGlyph.unicode = reversedMapping.get(glyphName)
def glyphControlPointBoundsRepresentationFactory(glyph): # base glyph pen = ControlBoundsPen(glyph.getParent()) glyph.draw(pen) bounds = pen.bounds # components for component in glyph.components: b = component.controlPointBounds if b is not None: if bounds is None: bounds = b else: bounds = unionRect(bounds, b) return bounds
def extractOpenTypeGlyphs(source, destination, layerName="public.default"): # grab the cmap vmtx = source.get("vmtx") vorg = source.get("VORG") is_ttf = "glyf" in source reversedMapping = source.get("cmap").buildReversed() # add layers if layerName != destination.defaultLayerName: destination.newLayer(layerName) # grab the glyphs glyphSet = source.getGlyphSet() for glyphName in glyphSet.keys(): sourceGlyph = glyphSet[glyphName] # make the new glyph destinationLayer = destination.getLayer(layerName) destinationLayer.newGlyph(glyphName) destinationGlyph = destinationLayer[glyphName] # outlines if is_ttf: pen = destinationGlyph.getPointPen() sourceGlyph.drawPoints(pen) else: pen = destinationGlyph.getPen() sourceGlyph.draw(pen) # width destinationGlyph.width = sourceGlyph.width # height and vertical origin if vmtx is not None and glyphName in vmtx.metrics: destinationGlyph.height = vmtx[glyphName][0] if vorg is not None: if glyphName in vorg.VOriginRecords: destinationGlyph.verticalOrigin = vorg[glyphName] else: destinationGlyph.verticalOrigin = vorg.defaultVertOriginY else: tsb = vmtx[glyphName][1] bounds_pen = ControlBoundsPen(glyphSet) sourceGlyph.draw(bounds_pen) if bounds_pen.bounds is None: continue xMin, yMin, xMax, yMax = bounds_pen.bounds destinationGlyph.verticalOrigin = tsb + yMax # unicodes destinationGlyph.unicodes = reversedMapping.get(glyphName, [])
def estimateCOLRv1BoundingBoxes(vcFont, ttFont, neutralOnly): from fontTools.pens.ttGlyphPen import TTGlyphPointPen from fontTools.pens.boundsPen import ControlBoundsPen locations = [{}] if not neutralOnly: for axis in ttFont["fvar"].axes: if axis.flags & 0x0001: # hidden axis continue values = {0} if axis.minValue < axis.defaultValue: values.add(-1) if axis.defaultValue < axis.maxValue: values.add(1) locations = [ dictUpdate(loc, axis.axisTag, v) for loc in locations for v in sorted(values) ] glyfTable = ttFont["glyf"] gvarTable = ttFont["gvar"] hmtxTable = ttFont["hmtx"] # TODO: fix tsb if we have "vmtx" for glyphName in sorted(vcFont.keys()): glyph = vcFont[glyphName] if not glyph.components or not glyph.outline.isEmpty(): continue # calculate the bounding box that would fit on all locations bpen = ControlBoundsPen(None) for loc in locations: vcFont.drawGlyph(bpen, glyphName, loc) gvarTable.variations.pop(glyphName, None) pen = TTGlyphPointPen(None) if bpen.bounds is not None: bounds = intRect(bpen.bounds) for pt in [bounds[:2], bounds[2:]]: pen.beginPath() pen.addPoint(pt, segmentType="line") pen.endPath() glyfTable[glyphName] = pen.glyph() adv, lsb = hmtxTable.metrics[glyphName] hmtxTable.metrics[glyphName] = adv, bounds[0]
def makeGlyphsBoundingBoxes(self): """ Make bounding boxes for all the glyphs, and return a dictionary of BoundingBox(xMin, xMax, yMin, yMax) namedtuples keyed by glyph names. The bounding box of empty glyphs (without contours or components) is set to None. Check that the float values are within the range of the specified self.roundTolerance, and if so use the rounded value; else take the floor or ceiling to ensure that the bounding box encloses the original values. """ def getControlPointBounds(glyph): pen.init() glyph.draw(pen) return pen.bounds def toInt(value, else_callback): rounded = round(value) if tolerance >= 0.5 or abs(rounded - value) <= tolerance: return rounded else: return int(else_callback(value)) tolerance = self.roundTolerance glyphBoxes = {} pen = ControlBoundsPen(self.allGlyphs) for glyphName, glyph in self.allGlyphs.items(): bounds = None if glyph or glyph.components: bounds = getControlPointBounds(glyph) if bounds: rounded = [] for value in bounds[:2]: rounded.append(toInt(value, math.floor)) for value in bounds[2:]: rounded.append(toInt(value, math.ceil)) bounds = BoundingBox(*rounded) glyphBoxes[glyphName] = bounds return glyphBoxes
def test_ignoreSinglePoint(self): pen = ControlBoundsPen(None, ignoreSinglePoints=True) pen.moveTo((0, 10)) self.assertEqual(None, pen.bounds)
def test_singlePoint(self): pen = ControlBoundsPen(None) pen.moveTo((-5, 10)) self.assertEqual("-5 10 -5 10", bounds_(pen))
def test_quadraticCurve(self): pen = ControlBoundsPen(None) pen.moveTo((0, 0)) pen.qCurveTo((6, 6), (10, 0)) self.assertEqual("0 0 10 6", bounds_(pen))
def test_curve(self): pen = ControlBoundsPen(None) pen.moveTo((0, 0)) pen.curveTo((20, 10), (90, 40), (0, 0)) self.assertEqual("0 0 90 40", bounds_(pen))
def contourControlPointBoundsRepresentationFactory(obj): pen = ControlBoundsPen(None) obj.draw(pen) return pen.bounds
def _get_controlPointBounds(self): pen = ControlBoundsPen(None) self.draw(pen) return pen.bounds
def componentPointBoundsRepresentationFactory(obj): pen = ControlBoundsPen(obj.layer) obj.draw(pen) return pen.bounds
def test_draw(self): pen = ControlBoundsPen(None) draw_(pen) self.assertEqual("-55 0 60 100", bounds_(pen))
def test_empty(self): pen = ControlBoundsPen(None) self.assertEqual(None, pen.bounds)
def getControlBounds(self, layer=None): pen = ControlBoundsPen(layer) # raise 'KeyError' when a referenced component is missing from glyph set pen.skipMissingComponents = False self.draw(pen) return None if pen.bounds is None else BoundingBox(*pen.bounds)
def glyphControlPointBoundsRepresentationFactory(glyph): pen = ControlBoundsPen(glyph.layer) glyph.draw(pen) return pen.bounds