Skip to content

inconsistent baselines for vertical texts #135

Open
@hajimehoshi

Description

@hajimehoshi

This is a remaining task from #124 (comment).

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!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions