Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

inconsistent baselines for vertical texts #135

Open
hajimehoshi opened this issue Jan 27, 2024 · 0 comments
Open

inconsistent baselines for vertical texts #135

hajimehoshi opened this issue Jan 27, 2024 · 0 comments

Comments

@hajimehoshi
Copy link
Contributor

hajimehoshi commented Jan 27, 2024

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant