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.