Closed hajimehoshi closed 9 months ago
I have confirmed this issue with the version 4f7d5afc5c9b0bd0eb674aca4e30362ad4272370.
Download NotoSansArabic-Regular.ttf from https://fonts.google.com/noto/specimen/Noto+Sans+Arabic/about, and run this program:
package main import ( "bufio" "bytes" _ "embed" "flag" "image" "image/draw" "image/png" "os" "slices" "github.com/go-text/typesetting/di" "github.com/go-text/typesetting/font" "github.com/go-text/typesetting/language" "github.com/go-text/typesetting/opentype/api" "github.com/go-text/typesetting/shaping" "golang.org/x/image/math/fixed" "golang.org/x/image/vector" ) //go:embed NotoSansArabic-Regular.ttf var notoSansArabic []byte type singleFontmap struct { face font.Face } func (s *singleFontmap) ResolveFace(r rune) font.Face { return s.face } func render(dst draw.Image, origX, origY float32, text string, useSegmenter bool) { f, err := font.ParseTTF(bytes.NewReader(notoSansArabic)) if err != nil { panic(err) } str := []rune(text) input := shaping.Input{ Text: str, RunStart: 0, RunEnd: len(str), Direction: di.DirectionRTL, Face: f, Size: fixed.I(32), Script: language.Arabic, Language: language.NewLanguage("ar"), } var inputs []shaping.Input if useSegmenter { var segmenter shaping.Segmenter inputs = segmenter.Split(input, &singleFontmap{face: f}) slices.Reverse(inputs) } else { inputs = append(inputs, input) } for _, input := range inputs { out := (&shaping.HarfbuzzShaper{}).Shape(input) for _, g := range out.Glyphs { segs := f.GlyphData(g.GlyphID).(api.GlyphOutline).Segments scaledSegs := make([]api.Segment, len(segs)) scale := fixed26_6ToFloat32(out.Size) / float32(f.Upem()) for i, seg := range segs { scaledSegs[i] = seg for j := range seg.Args { scaledSegs[i].Args[j].X *= scale scaledSegs[i].Args[j].Y *= -scale } } drawSegments(dst, origX+fixed26_6ToFloat32(g.XOffset), origY+fixed26_6ToFloat32(-g.YOffset), scaledSegs) origX += fixed26_6ToFloat32(g.XAdvance) } } } func fixed26_6ToFloat32(x fixed.Int26_6) float32 { return float32(x) / (1 << 6) } func drawSegments(dst draw.Image, origX, origY float32, segs []api.Segment) { if len(segs) == 0 { return } rast := vector.NewRasterizer(dst.Bounds().Max.X, dst.Bounds().Max.Y) for _, seg := range segs { switch seg.Op { case api.SegmentOpMoveTo: rast.MoveTo(seg.Args[0].X+origX, seg.Args[0].Y+origY) case api.SegmentOpLineTo: rast.LineTo(seg.Args[0].X+origX, seg.Args[0].Y+origY) case api.SegmentOpQuadTo: rast.QuadTo( seg.Args[0].X+origX, seg.Args[0].Y+origY, seg.Args[1].X+origX, seg.Args[1].Y+origY, ) case api.SegmentOpCubeTo: rast.CubeTo( seg.Args[0].X+origX, seg.Args[0].Y+origY, seg.Args[1].X+origX, seg.Args[1].Y+origY, seg.Args[2].X+origX, seg.Args[2].Y+origY, ) } } rast.ClosePath() rast.DrawOp = draw.Over rast.Draw(dst, dst.Bounds(), image.Opaque, image.Point{}) } var ( flagSegmenter = flag.Bool("segmenter", false, "use segmenter") ) func main() { flag.Parse() dst := image.NewRGBA(image.Rect(0, 0, 640, 480)) draw.Draw(dst, dst.Bounds(), image.Black, image.Point{}, draw.Src) text := "لمّا" render(dst, 50, 100, text, *flagSegmenter) f, err := os.Create("output.png") if err != nil { panic(err) } defer f.Close() out := bufio.NewWriter(f) defer out.Flush() if err := png.Encode(out, dst); err != nil { panic(err) } }
Without splitting, the rendering result seems fine. This matches the rendering result of Chrome.
go run main.go
With splitting, the rendering result is a little different.
go run main.go -segmenter
Nice catch !
I have confirmed this issue with the version 4f7d5afc5c9b0bd0eb674aca4e30362ad4272370.
Download NotoSansArabic-Regular.ttf from https://fonts.google.com/noto/specimen/Noto+Sans+Arabic/about, and run this program:
Without splitting, the rendering result seems fine. This matches the rendering result of Chrome.
With splitting, the rendering result is a little different.