Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions internal/book/epub_font.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ var epubFallbackFonts = sync.OnceValues(findEPUBFallbackFonts)

func findEPUBFallbackFonts() ([]*font.Font, error) {
if path := os.Getenv("CBZR_EPUB_FALLBACK_FONT"); path != "" {
fonts, err := readEPUBFallbackFonts(path)
fonts, err := readEPUBFallbackFonts(path, maxEPUBFontFaces)
if err != nil {
return nil, fmt.Errorf("EPUB fallback font: %w", err)
}
Expand Down Expand Up @@ -74,16 +74,20 @@ func findEPUBFallbackFonts() ([]*font.Font, error) {
"/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc",
}
}
// The faces of these collections are regional or metric variants of the
// same characters. Each parsed face copies the shared tables, which cost
// about 200 MB for all ten faces of Noto Sans CJK, so one face is enough.
var fonts []*font.Font
for _, path := range paths {
if found, err := readEPUBFallbackFonts(path); err == nil {
if found, err := readEPUBFallbackFonts(path, 1); err == nil {
fonts = append(fonts, found...)
}
}
return fonts, nil
}

func readEPUBFallbackFonts(path string) ([]*font.Font, error) {
// readEPUBFallbackFonts parses up to faces faces of a font file.
func readEPUBFallbackFonts(path string, faces int) ([]*font.Font, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
Expand All @@ -93,10 +97,11 @@ func readEPUBFallbackFonts(path string) ([]*font.Font, error) {
if err != nil {
return nil, err
}
return parseEPUBFonts(data)
return parseEPUBFonts(data, faces)
}

func parseEPUBFonts(data []byte) ([]*font.Font, error) {
// parseEPUBFonts parses up to faces faces of a TTF, OTF or collection.
func parseEPUBFonts(data []byte, faces int) ([]*font.Font, error) {
if len(data) < 4 {
return nil, fmt.Errorf("font must be TTF/OTF or TTC/OTC")
}
Expand All @@ -119,6 +124,7 @@ func parseEPUBFonts(data []byte) ([]*font.Font, error) {
if len(loaders) < 1 || len(loaders) > maxEPUBFontFaces {
return nil, fmt.Errorf("font collection must contain 1 to %d faces", maxEPUBFontFaces)
}
loaders = loaders[:min(len(loaders), faces)]
fonts := make([]*font.Font, 0, len(loaders))
for _, loader := range loaders {
f, err := font.NewFont(loader)
Expand All @@ -134,7 +140,7 @@ func parseEPUBFonts(data []byte) ([]*font.Font, error) {
var epubBundledFonts = sync.OnceValues(func() ([]*font.Font, error) {
var fonts []*font.Font
for _, data := range [][]byte{goregular.TTF, gobold.TTF, goitalic.TTF, gobolditalic.TTF} {
parsed, err := parseEPUBFonts(data)
parsed, err := parseEPUBFonts(data, 1)
if err != nil {
return nil, err
}
Expand Down
11 changes: 5 additions & 6 deletions internal/book/epub_font_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
)

func TestEPUBFallbackShapedRaster(t *testing.T) {
fonts, err := parseEPUBFonts(goregular.TTF)
fonts, err := parseEPUBFonts(goregular.TTF, 1)
if err != nil {
t.Fatal(err)
}
Expand All @@ -33,11 +33,6 @@ func TestEPUBFallbackShapedRaster(t *testing.T) {
if len(runs) != 3 || runs[0].font != &primary || runs[1].font != fonts[0] || runs[2].font != &primary {
t.Fatalf("wrong fallback runs: %#v", runs)
}
for _, run := range runs {
if run.Face != nil {
t.Fatal("page retained mutable layout face")
}
}
before, _, err := l.book.PageBytes(0)
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -121,6 +116,10 @@ func TestEPUBFontCollection(t *testing.T) {
if err != nil || len(fonts) != count {
t.Fatalf("font collection: %d fonts, %v", len(fonts), err)
}
// System collections only need their first face.
if first, err := parseEPUBFonts(data, 1); err != nil || len(first) != 1 {
t.Fatalf("first face of a collection: %d fonts, %v", len(first), err)
}
for _, f := range fonts {
l := &epubLayout{book: &Book{}, regular: []*font.Face{font.NewFace(f)}, y: epubMargin}
if err := l.text("A", false); err != nil {
Expand Down
47 changes: 28 additions & 19 deletions internal/book/epub_layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,43 +248,52 @@ func epubClusterFaces(inputs []shaping.Input, text []rune, faces []*font.Face) [
func freezeEPUBLine(line shaping.Line, text string) epubLine {
line = slices.Clone(line)
slices.SortFunc(line, func(a, b shaping.Output) int { return int(a.VisualIndex - b.VisualIndex) })
result := epubLine{text: text}
result := epubLine{text: text, runs: make([]epubRun, 0, len(line))}
var x fixed.Int26_6
for _, output := range line {
f := output.Face.Font
output.Face = nil
output.Glyphs = slices.Clone(output.Glyphs)
result.runs = append(result.runs, epubRun{Output: output, font: f, x: x})
run := epubRun{
font: output.Face.Font, x: x, advance: output.Advance, size: output.Size, direction: output.Direction,
ascent: max(output.LineBounds.Ascent.Ceil(), output.GlyphBounds.Ascent.Ceil()),
descent: max((-output.LineBounds.Descent).Ceil(), (-output.GlyphBounds.Descent).Ceil()),
glyphs: make([]epubGlyph, len(output.Glyphs)),
}
var gx fixed.Int26_6
for i, g := range output.Glyphs {
left := gx + g.XOffset + g.XBearing
if i == 0 || left < run.inkLeft {
run.inkLeft = left
}
if i == 0 || left+g.Width > run.inkRight {
run.inkRight = left + g.Width
}
run.glyphs[i] = epubGlyph{g.GlyphID, g.Advance, g.XOffset, g.YOffset, int32(g.TextIndex()), int32(g.RunesCount())}
gx += g.Advance
}
result.runs = append(result.runs, run)
x += output.Advance
}
return result
}

func epubLineBounds(line epubLine) (ascent, descent int) {
for _, run := range line.runs {
ascent = max(ascent, run.LineBounds.Ascent.Ceil(), run.GlyphBounds.Ascent.Ceil())
descent = max(descent, (-run.LineBounds.Descent).Ceil(), (-run.GlyphBounds.Descent).Ceil())
ascent, descent = max(ascent, run.ascent), max(descent, run.descent)
}
return
}

func epubLineWidth(line epubLine) fixed.Int26_6 {
var width fixed.Int26_6
for _, run := range line.runs {
width = max(width, run.x+run.Advance)
width = max(width, run.x+run.advance)
}
return width
}

func epubLineFits(line epubLine) bool {
for _, run := range line.runs {
x := run.x
for _, glyph := range run.Glyphs {
left := x + glyph.XOffset + glyph.XBearing
if left < -fixed.I(epubMargin) || left+glyph.Width > fixed.I(epubPageWidth-epubMargin) {
return false
}
x += glyph.Advance
if len(run.glyphs) > 0 && (run.x+run.inkLeft < -fixed.I(epubMargin) || run.x+run.inkRight > fixed.I(epubPageWidth-epubMargin)) {
return false
}
}
return true
Expand Down Expand Up @@ -336,12 +345,12 @@ func (l *epubLayout) paragraph(p epubParagraph, heading bool) error {
left, right := fixed.I(epubPageWidth), fixed.Int26_6(0)
for _, run := range line.runs {
x := run.x
for _, glyph := range run.Glyphs {
if glyph.TextIndex() < ruby.end && glyph.TextIndex()+glyph.RunesCount() > ruby.start {
for _, glyph := range run.glyphs {
if int(glyph.index) < ruby.end && int(glyph.index+glyph.runes) > ruby.start {
left = min(left, x)
right = max(right, x+glyph.Advance)
right = max(right, x+glyph.advance)
}
x += glyph.Advance
x += glyph.advance
}
}
if right <= left {
Expand Down
4 changes: 2 additions & 2 deletions internal/book/epub_layout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (

func bundledEPUBLayout(t *testing.T) *epubLayout {
t.Helper()
fonts, err := parseEPUBFonts(goregular.TTF)
fonts, err := parseEPUBFonts(goregular.TTF, 1)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -100,7 +100,7 @@ func TestEPUBRubyInlineAndPageBounds(t *testing.T) {
t.Fatal("ruby outside annotation band")
}
for _, run := range annotation.runs {
if run.Size != fixed.I(16) {
if run.size != fixed.I(16) {
t.Fatal("reading is not smaller")
}
}
Expand Down
57 changes: 41 additions & 16 deletions internal/book/epub_render.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"strings"
"sync"

"github.com/go-text/typesetting/di"
"github.com/go-text/typesetting/font"
"github.com/go-text/typesetting/font/opentype"
"github.com/go-text/typesetting/shaping"
Expand All @@ -26,9 +27,20 @@ const (
)

type epubRun struct {
shaping.Output
font *font.Font
x fixed.Int26_6
font *font.Font
x, advance, size fixed.Int26_6
direction di.Direction
ascent, descent int // pixels above and below the baseline
inkLeft, inkRight fixed.Int26_6 // glyph ink extent from x
glyphs []epubGlyph
}

// epubGlyph keeps what painting and selection read of a shaped glyph in 24
// bytes. shaping.Glyph takes 80, and a long novel lays out millions.
type epubGlyph struct {
id font.GID
advance, xOffset, yOffset fixed.Int26_6
index, runes int32 // rune range in the paragraph
}

type epubLine struct {
Expand Down Expand Up @@ -73,24 +85,37 @@ func (p epubTextPage) render(ctx context.Context) ([]byte, error) {
face = font.NewFace(run.font)
faces[run.font] = face
}
scale := float32(run.Size) / 64 / float32(face.Upem())
scale := float32(run.size) / 64 / float32(face.Upem())
x := float32(epubMargin) + float32(run.x)/64
for _, glyph := range run.Glyphs {
for _, glyph := range run.glyphs {
if err := ctx.Err(); err != nil {
return err
}
if glyph.GlyphID != font.EmptyGlyph {
outline, ok := face.GlyphDataOutline(glyph.GlyphID)
ox := x + float32(glyph.XOffset)/64
oy := float32(line.y) - float32(glyph.YOffset)/64
if glyph.id != font.EmptyGlyph {
outline, ok := face.GlyphDataOutline(glyph.id)
if !ok || len(outline.Segments) == 0 {
x += float32(glyph.advance) / 64
continue
}
ox := x + float32(glyph.xOffset)/64
oy := float32(line.y) - float32(glyph.yOffset)/64
// The outline's points enclose its curves, so they bound the ink.
minX, minY := float32(math.Inf(1)), float32(math.Inf(1))
maxX, maxY := float32(math.Inf(-1)), float32(math.Inf(-1))
for i := range outline.Segments {
for _, p := range outline.Segments[i].ArgsSlice() {
minX, maxX = min(minX, p.X), max(maxX, p.X)
minY, maxY = min(minY, p.Y), max(maxY, p.Y)
}
}
bounds := image.Rect(
int(math.Floor(float64(ox+float32(glyph.XBearing)/64)))-1,
int(math.Floor(float64(oy-float32(glyph.YBearing)/64)))-1,
int(math.Ceil(float64(ox+float32(glyph.XBearing+glyph.Width)/64)))+1,
int(math.Ceil(float64(oy-float32(glyph.YBearing+glyph.Height)/64)))+1,
int(math.Floor(float64(ox+minX*scale)))-1,
int(math.Floor(float64(oy-maxY*scale)))-1,
int(math.Ceil(float64(ox+maxX*scale)))+1,
int(math.Ceil(float64(oy-minY*scale)))+1,
).Intersect(canvas.Bounds())
if !ok || len(outline.Segments) == 0 || bounds.Empty() {
x += float32(glyph.Advance) / 64
if bounds.Empty() {
x += float32(glyph.advance) / 64
continue
}
ox -= float32(bounds.Min.X)
Expand All @@ -117,7 +142,7 @@ func (p epubTextPage) render(ctx context.Context) ([]byte, error) {
raster.ClosePath()
raster.Draw(canvas, bounds, image.Black, image.Point{})
}
x += float32(glyph.Advance) / 64
x += float32(glyph.advance) / 64
}
}
for _, annotation := range line.annotations {
Expand Down
8 changes: 4 additions & 4 deletions internal/book/select.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,10 @@ func (p epubTextPage) textLines() []textLine {
out := textLine{text: line.text, top: float64(top) / epubPageHeight, bottom: float64(bottom) / epubPageHeight, end: line.end}
for _, run := range line.runs {
x := fixed.I(epubMargin) + run.x
rtl := run.Direction.Progression() == di.TowardTopLeft
for _, glyph := range run.Glyphs {
out.boxes = append(out.boxes, textBox{epubFraction(x), epubFraction(x + glyph.Advance), glyph.TextIndex() - line.offset, glyph.RunesCount(), rtl})
x += glyph.Advance
rtl := run.direction.Progression() == di.TowardTopLeft
for _, glyph := range run.glyphs {
out.boxes = append(out.boxes, textBox{epubFraction(x), epubFraction(x + glyph.advance), int(glyph.index) - line.offset, int(glyph.runes), rtl})
x += glyph.advance
}
}
for _, link := range line.links {
Expand Down
2 changes: 1 addition & 1 deletion internal/native/native_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ func (r *reader) webtoonFrame(width, height int) (*image.RGBA, error) {
}
img, page, offset, err := render.ComposeWebtoon(
load, r.book.Len(), r.state.Page, r.state.Offset, scrollPixels,
width, height, 0, 1, 1, nil,
width, height, 0, 1, 1, 1, nil,
)
if err == nil {
if page == r.state.Page && math.Abs(offset-r.state.Offset) < 1e-9 {
Expand Down
5 changes: 4 additions & 1 deletion internal/render/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ func getRGBA(r image.Rectangle) *image.RGBA {

// Recycle hands back a frame from ComposeWebtoon, Invert or Highlight once
// nothing refers to it any more.
func Recycle(img *image.RGBA) { rgbaPool.Put(img) }
func Recycle(img *image.RGBA) {
forgetRotations(img)
rgbaPool.Put(img)
}

// fitBox scales iw x ih pixels to fit maxCols x maxRows cells of cw x ch
// pixels each, preserving aspect ratio. Returns the cell grid and pixel size.
Expand Down
Loading
Loading