...because your users shouldn't need to connect to someone else's server, just to display a few (static) badges in your README
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

150 lines
4.4 KiB

* Copyright 2021 Lucas Hinderberger
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* See the License for the specific language governing permissions and
* limitations under the License.
package text2path
import (
pkgfont "golang.org/x/image/font"
// FontMetrics describes a number of important sizes for the rendering of
// a particular font.
// It wraps the original pkgfont.Metrics struct from which it has been converted.
type FontMetrics struct {
Original pkgfont.Metrics
Height float32
Ascent float32
Descent float32
XHeight float32
type RenderResult struct {
// The paths of the rendered text, as an SVG fragment
Paths string
// The width of the rendered text
Width float32
// CalculateFontMetrics retrieves the font metrics from the given font and returns them
// converted to float32
func CalculateFontMetrics(font *sfnt.Font, fontSize float32) (*FontMetrics, error) {
var buf sfnt.Buffer
fixedFontSize := floatToFixed(fontSize)
metrics, err := font.Metrics(&buf, fixedFontSize, pkgfont.HintingNone)
if err != nil {
return nil, err
return &FontMetrics{
Original: metrics,
Height: fixedToFloat(metrics.Height),
Ascent: fixedToFloat(metrics.Ascent),
Descent: fixedToFloat(metrics.Descent),
XHeight: fixedToFloat(metrics.XHeight),
}, nil
// RenderStringToSVG uses the given font to render the given string into
// SVG paths at the given font size (pixels per em).
// It returns the SVG path elements that represent the given string, without an
// enclosing element and the total width of the rendered text.
func RenderStringToSVG(font *sfnt.Font, text string, fontSize float32, fillColor string) (*RenderResult, error) {
var builder strings.Builder
var buf sfnt.Buffer
fixedFontSize := floatToFixed(fontSize)
totalAdvance := float32(0)
for _, r := range text {
idx, err := font.GlyphIndex(&buf, r)
if err != nil {
return nil, err
segments, err := font.LoadGlyph(&buf, idx, fixedFontSize, &sfnt.LoadGlyphOptions{})
if err != nil {
return nil, err
fixedAdvance, err := font.GlyphAdvance(&buf, idx, fixedFontSize, pkgfont.HintingNone)
if err != nil {
return nil, err
advance := fixedToFloat(fixedAdvance)
pathData := SegmentsToPathData(segments)
// Note that errors are ignored here, as strings.Builder cannot return write errors, and thus Fprintf won't do either
_, _ = fmt.Fprintf(&builder, `<path transform="translate(%f,0)" d="%s" fill="%s" />`, totalAdvance, pathData, fillColor)
totalAdvance += advance
return &RenderResult{
Paths: builder.String(),
Width: totalAdvance,
}, nil
// SegmentsToPathData builds the input to an SVG path element's "d" property
// from the given glyph segments
func SegmentsToPathData(segments sfnt.Segments) string {
var builder strings.Builder
for _, segment := range segments {
var floatArgs [3][2]float32
for i, arg := range segment.Args {
floatArgs[i][0] = fixedToFloat(arg.X)
floatArgs[i][1] = fixedToFloat(arg.Y)
// Note that errors are ignored here, as strings.Builder cannot return write errors, and thus Fprintf won't do either
switch segment.Op {
case sfnt.SegmentOpMoveTo:
_, _ = fmt.Fprintf(&builder, " M %f,%f ", floatArgs[0][0], floatArgs[0][1])
case sfnt.SegmentOpLineTo:
_, _ = fmt.Fprintf(&builder, " L %f,%f ", floatArgs[0][0], floatArgs[0][1])
case sfnt.SegmentOpQuadTo:
_, _ = fmt.Fprintf(&builder, " Q %f,%f %f,%f ", floatArgs[0][0], floatArgs[0][1], floatArgs[1][0], floatArgs[1][1])
case sfnt.SegmentOpCubeTo:
_, _ = fmt.Fprintf(&builder, " C %f,%f %f,%f %f,%f ", floatArgs[0][0], floatArgs[0][1], floatArgs[1][0], floatArgs[1][1], floatArgs[2][0], floatArgs[2][1])
return builder.String()
func fixedToFloat(in fixed.Int26_6) float32 {
return float32(in>>6) + float32(in&0x0000003F)/64.0
func floatToFixed(in float32) fixed.Int26_6 {
whole := int32(in)
part := int32((in - float32(whole)) * 64)
return fixed.Int26_6(whole<<6 | part)