github.com/kintar/etxt@v0.0.9/emetric/metric.go (about)

     1  // A collection of helper functions for examining certain font or glyph
     2  // properties, irrelevant unless you are really deep into this mess.
     3  //
     4  // This subpackage is *not* used in etxt itself.
     5  package emetric
     6  
     7  import "golang.org/x/image/math/fixed"
     8  import "golang.org/x/image/font/sfnt"
     9  
    10  // cboxObox computes two bounding boxes for the given segments:
    11  //   - The [control box], equivalent to sfnt.Segments.Bounds().
    12  //   - The "ON" contour points bounding box.
    13  //
    14  // These two can be used by CBoxBadness to determine if the CBox
    15  // matches the real bounding box or not (though the actual bounding
    16  // box can't be easily determined if the two are different).
    17  //
    18  // [control box]: https://freetype.org/freetype2/docs/glyphs/glyphs-6.html#section-2
    19  func cboxObox(segments sfnt.Segments) (fixed.Rectangle26_6, fixed.Rectangle26_6) {
    20  	// create boxes
    21  	cbox := fixed.Rectangle26_6{
    22  		Min: fixed.Point26_6{
    23  			X: fixed.Int26_6(0x7FFFFFFF),
    24  			Y: fixed.Int26_6(0x7FFFFFFF),
    25  		},
    26  		Max: fixed.Point26_6{
    27  			X: fixed.Int26_6(-0x80000000),
    28  			Y: fixed.Int26_6(-0x80000000),
    29  		},
    30  	}
    31  	obox := fixed.Rectangle26_6{
    32  		Min: fixed.Point26_6{X: cbox.Min.X, Y: cbox.Min.Y},
    33  		Max: fixed.Point26_6{X: cbox.Max.X, Y: cbox.Max.Y},
    34  	}
    35  
    36  	// iterate segments
    37  	for _, segment := range segments {
    38  		switch segment.Op {
    39  		case sfnt.SegmentOpMoveTo, sfnt.SegmentOpLineTo:
    40  			adjustBoxLimits(&cbox, segment.Args[0:1])
    41  			adjustBoxLimits(&obox, segment.Args[0:1])
    42  		case sfnt.SegmentOpQuadTo:
    43  			adjustBoxLimits(&cbox, segment.Args[0:2])
    44  			adjustBoxLimits(&obox, segment.Args[1:2])
    45  		case sfnt.SegmentOpCubeTo:
    46  			adjustBoxLimits(&cbox, segment.Args[0:3])
    47  			adjustBoxLimits(&obox, segment.Args[2:3])
    48  		default:
    49  			panic("unexpected segment.Op")
    50  		}
    51  	}
    52  	return cbox, obox
    53  }
    54  
    55  func adjustBoxLimits(box *fixed.Rectangle26_6, points []fixed.Point26_6) {
    56  	for _, point := range points {
    57  		if box.Max.X < point.X {
    58  			box.Max.X = point.X
    59  		}
    60  		if box.Min.X > point.X {
    61  			box.Min.X = point.X
    62  		}
    63  		if box.Max.Y < point.Y {
    64  			box.Max.Y = point.Y
    65  		}
    66  		if box.Min.Y > point.Y {
    67  			box.Min.Y = point.Y
    68  		}
    69  	}
    70  }
    71  
    72  // Computes how much the [control box] of the given segments exceeds
    73  // the box defined by the "ON" contour points. Whenever there's an
    74  // excess, that means that the control box doesn't match the bounding
    75  // box of the glyph segments, which might have unintended effects in
    76  // the rendering position of the glyph. Though you'd have to be crazy
    77  // to care much about this, as the effect is almost always way smaller
    78  // than typical hinting distortions. So, visually you are unlikely to
    79  // see anything at all even if CBoxBadness are non-zero... but it has
    80  // implications for technical correctness of computed left and right
    81  // side bearings and stuff like that if you are obsessive enough.
    82  //
    83  // Returned badnesses are left, right, top and bottom, and the values
    84  // can only be zero or positive.
    85  //
    86  // [control box]: https://freetype.org/freetype2/docs/glyphs/glyphs-6.html#section-2
    87  func CBoxBadness(segments sfnt.Segments) (fixed.Int26_6, fixed.Int26_6, fixed.Int26_6, fixed.Int26_6) {
    88  	cbox, obox := cboxObox(segments)
    89  	leftBadness := -cbox.Min.X + cbox.Min.X
    90  	rightBadness := cbox.Max.X - obox.Max.X
    91  	topBadness := -cbox.Min.Y + obox.Min.Y
    92  	bottomBadness := cbox.Max.Y - obox.Max.Y
    93  	return leftBadness, rightBadness, topBadness, bottomBadness
    94  }
    95  
    96  // Returns the ascent of the given rune both as units and as the ratio
    97  // to the font's em square size. In general, capital latin latters will
    98  // return ratios around 0.7, while lowercase letters like 'a', 'x', 'r'
    99  // and similar will return ratios around 0.48. But anything is possible,
   100  // really.
   101  //
   102  // The buffer can be nil.
   103  func RuneAscent(font *sfnt.Font, codePoint rune, buffer *sfnt.Buffer) (sfnt.Units, float64, error) {
   104  	if buffer == nil {
   105  		buffer = &sfnt.Buffer{}
   106  	}
   107  	unitSize := fixed.Int26_6(font.UnitsPerEm())
   108  	glyphIndex, err := font.GlyphIndex(buffer, codePoint)
   109  	if err != nil {
   110  		return 0, 0, err
   111  	}
   112  	contours, err := font.LoadGlyph(buffer, glyphIndex, unitSize, nil)
   113  	if err != nil {
   114  		return 0, 0, err
   115  	}
   116  	ascentUnits := -contours.Bounds().Min.Y
   117  	emProportion := float64(ascentUnits) / float64(unitSize)
   118  	return sfnt.Units(ascentUnits), emProportion, nil
   119  }
   120  
   121  // TODO: a true BBox(segments sfnt.Segments) implementation
   122  //       do double or triple pass. if cboxBadness is 0, we
   123  //       already know the BBox. otherwise, do first a general
   124  //       approximation with the normal segments and points, and
   125  //       only at the end check if the bézier curves that may
   126  //       affect the final bounding box actually affect it. or
   127  //       just look into freetype implementation.