go-text / typesetting

High quality text shaping in pure Go.
Other
100 stars 10 forks source link

using `Segmenter.Split` causes a slightly different rendering result #127

Closed hajimehoshi closed 9 months ago

hajimehoshi commented 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

image

With splitting, the rendering result is a little different.

go run main.go -segmenter

image

benoitkugler commented 9 months ago

Nice catch !