go-text / typesetting

High quality text shaping in pure Go.
Other
88 stars 11 forks source link

inconsistent baselines for vertical texts #135

Open hajimehoshi opened 5 months ago

hajimehoshi commented 5 months ago

This is a remaining task from https://github.com/go-text/typesetting/pull/124#issuecomment-1856227000.

I used NotoSansJP-VF.otf from https://github.com/notofonts/noto-cjk/releases/tag/Sans2.004 ("All Variables OTF/OTC)".

package main

import (
    "bufio"
    "bytes"
    _ "embed"
    "flag"
    "image"
    "image/draw"
    "image/png"
    "os"
    "strings"

    "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 NotoSansJP-VF.otf
var notoSansJP []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) {
    f, err := font.ParseTTF(bytes.NewReader(notoSansJP))
    if err != nil {
        panic(err)
    }
    script, err := language.ParseScript("jpan")
    if err != nil {
        panic(err)
    }
    str := []rune(text)
    input := shaping.Input{
        Text:      str,
        RunStart:  0,
        RunEnd:    len(str),
        Direction: di.DirectionTTB,
        Face:      f,
        Size:      fixed.I(32),
        Script:    script,
        Language:  language.NewLanguage("jp"),
    }

    var segmenter shaping.Segmenter
    inputs := segmenter.Split(input, &singleFontmap{face: f})

    for _, input := range inputs {
        out := (&shaping.HarfbuzzShaper{}).Shape(input)
        (shaping.Line{out}).AdjustBaselines()
        for _, g := range out.Glyphs {
            data := f.GlyphData(g.GlyphID).(api.GlyphOutline)
            if out.Direction.IsSideways() {
                data.Sideways(fixed26_6ToFloat32(-g.YOffset) / fixed26_6ToFloat32(out.Size) * float32(f.Upem()))
            }
            segs := data.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)
            origY += fixed26_6ToFloat32(-g.YAdvance)
        }
    }
}

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{})
}

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 := "あgo-textあ\nあisあ\nあawesomeあ"
    for i, line := range strings.Split(text, "\n") {
        render(dst, 400-float32(i)*40, 100, line)
    }

    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)
    }

}
module foo

go 1.21.6

require (
    github.com/go-text/typesetting v0.1.1-0.20231231232151-8d81c02dc157
    golang.org/x/image v0.15.0
)

require golang.org/x/text v0.14.0 // indirect

The output is:

image

The baselines for "go-text", "is", and "awesome" are different for each line.

The expected result is what Chrome browser does:

<style>
  body {
    font-size: 32px;
    font-family: sans-serif;
    writing-mode: vertical-rl;
  }
</style>
<p>あgo-textあ<br>
あisあ<br>
あawersomeあ</p>

image

Thanks!