mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
This commit is contained in:
1 parent
c15547f0d2
commit
f44d0d8a60
30 files changed
+2075
-463
No files matched your search
@@ -0,0 +1,373 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PositionedGlyph 保存页面坐标字形与对应原文区间
|
||||
type PositionedGlyph struct {
|
||||
Path GeometryPath
|
||||
Underline GeometryPath
|
||||
Start, End int
|
||||
}
|
||||
|
||||
// PositionedText 保存绘制、搜索和度量共同使用的定位结果
|
||||
// Glyphs中的路径和Clip为只读页面坐标,Run保留原文而非包装字符
|
||||
type PositionedText struct {
|
||||
Glyphs []PositionedGlyph
|
||||
Run TextRun
|
||||
Clip *GeometryPath
|
||||
Matrix Matrix
|
||||
}
|
||||
|
||||
// glyphOutlineKey 区分字体、字形与毫米字号
|
||||
type glyphOutlineKey struct {
|
||||
font *PreparedFont
|
||||
glyph uint16
|
||||
size float64
|
||||
}
|
||||
|
||||
// preparedMetrics 复用已包装字体的后端度量,不混用不同后端实例
|
||||
// 入参: prepared 已包装字体
|
||||
// 返回: FontMetrics 字体度量, error 字体解析错误
|
||||
func (r *Renderer) preparedMetrics(prepared *PreparedFont) (FontMetrics, error) {
|
||||
if metrics := r.fontMetrics[prepared]; metrics != nil {
|
||||
return metrics, nil
|
||||
}
|
||||
metrics, err := r.backends.Fonts.OpenFont(prepared.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.fontMetrics[prepared] = metrics
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// preparedOutline 按字形和字号复用只读轮廓
|
||||
// 入参: prepared 字体, metrics 度量, glyph 字形编号, size 毫米字号
|
||||
// 返回: GeometryPath 字形路径, error 能力或解析错误
|
||||
func (r *Renderer) preparedOutline(prepared *PreparedFont, metrics FontMetrics, glyph uint16, size float64) (GeometryPath, error) {
|
||||
key := glyphOutlineKey{prepared, glyph, size}
|
||||
if path, ok := r.glyphOutlines[key]; ok {
|
||||
return path, nil
|
||||
}
|
||||
provider, ok := metrics.(FontOutlines)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("font outlines: %w", ErrBackendUnavailable)
|
||||
}
|
||||
path, err := provider.GlyphOutline(glyph, size)
|
||||
if err == nil {
|
||||
r.glyphOutlines[key] = path
|
||||
}
|
||||
return path, err
|
||||
}
|
||||
|
||||
// renderObjectMatrix 统一边界、局部变换与父变换的组合顺序
|
||||
// 入参: boundary 对象边界, local 局部矩阵, state 继承状态
|
||||
// 返回: Matrix 页面矩阵, Matrix 不含边界平移的组合矩阵
|
||||
func renderObjectMatrix(boundary string, local Matrix, state RenderState) (Matrix, Matrix) {
|
||||
box, _ := ParseBox(boundary)
|
||||
linear := local
|
||||
if state.Parent != nil {
|
||||
linear = state.Parent.Multiply(local)
|
||||
}
|
||||
matrix := TranslationMatrix(box.X, box.Y).Multiply(linear)
|
||||
if state.BoundaryInCTM && state.Parent != nil {
|
||||
matrix = state.Parent.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(local)
|
||||
}
|
||||
return matrix, linear
|
||||
}
|
||||
|
||||
// objectGeometryClip 解析对象裁剪并合并父裁剪
|
||||
// 入参: clips 对象裁剪, boundary 边界, local 局部矩阵, state 继承状态
|
||||
// 返回: *GeometryPath 页面裁剪, error 几何错误
|
||||
func (r *Renderer) objectGeometryClip(clips *Clips, boundary string, local Matrix, state RenderState) (*GeometryPath, error) {
|
||||
if clips == nil {
|
||||
return state.Clip, nil
|
||||
}
|
||||
matrix, _ := renderObjectMatrix(boundary, local, state)
|
||||
if clips.TransFlag != nil && !*clips.TransFlag {
|
||||
copy := *clips
|
||||
copy.TransFlag = nil
|
||||
clips = ©
|
||||
box, _ := ParseBox(boundary)
|
||||
matrix = TranslationMatrix(box.X, box.Y)
|
||||
if state.BoundaryInCTM && state.Parent != nil {
|
||||
matrix = state.Parent.Multiply(matrix)
|
||||
}
|
||||
}
|
||||
geometry, err := r.Geometry()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return geometry.Clip(r, clips, matrix, state.Clip)
|
||||
}
|
||||
|
||||
// clipGeometry 将非零填充路径与页面裁剪相交
|
||||
// 入参: geometry 几何后端, path 页面路径, clip 页面裁剪
|
||||
// 返回: GeometryPath 裁剪路径, error 几何错误
|
||||
func clipGeometry(geometry GeometryBackend, path GeometryPath, clip *GeometryPath) (GeometryPath, error) {
|
||||
if clip == nil {
|
||||
return path, nil
|
||||
}
|
||||
return geometry.Combine(path, *clip, GeometryIntersect)
|
||||
}
|
||||
|
||||
// PositionText 定位文字、字形与搜索范围,不依赖页面输出后端
|
||||
// 入参: object 文字对象, state 继承状态
|
||||
// 返回: *PositionedText 定位结果, error 字体或几何错误
|
||||
func (r *Renderer) PositionText(object TextObject, state RenderState) (*PositionedText, error) {
|
||||
result := &PositionedText{Run: TextRun{ID: object.ID, Text: object.Text()}}
|
||||
result.Run.Boxes = make([]Box, len([]rune(result.Run.Text)))
|
||||
if object.Visible != nil && !*object.Visible {
|
||||
return result, nil
|
||||
}
|
||||
geometry, err := r.Geometry()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
local := NewMatrix(object.CTM)
|
||||
matrix, linear := renderObjectMatrix(object.Boundary, local, state)
|
||||
result.Matrix = matrix
|
||||
result.Clip, err = r.objectGeometryClip(object.Clips, object.Boundary, local, state)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defaults := r.drawParamDefaults(object.DrawParam, state.Defaults)
|
||||
size := object.Size
|
||||
if size == 0 && defaults != nil {
|
||||
size = defaults.Size
|
||||
}
|
||||
if size == 0 {
|
||||
size = 3.5
|
||||
}
|
||||
if object.VScale != 0 {
|
||||
size *= object.VScale
|
||||
}
|
||||
horizontal := object.HScale
|
||||
if horizontal == 0 {
|
||||
horizontal = 1
|
||||
}
|
||||
id := object.Font
|
||||
if id == "" && defaults != nil {
|
||||
id = defaults.Font
|
||||
}
|
||||
prepared, err := r.PrepareFont(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(prepared.Data) == 0 {
|
||||
box, _ := ParseBox(object.Boundary)
|
||||
boundary, _ := renderObjectMatrix(object.Boundary, IdentityMatrix, state)
|
||||
path, err := geometry.Transform(geometryRectangle(Box{W: box.W, H: box.H}), boundary)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path, err = clipGeometry(geometry, path, result.Clip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
box, err = geometry.Bounds(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range result.Run.Boxes {
|
||||
result.Run.Boxes[i] = box
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
metrics, err := r.preparedMetrics(prepared)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
weight, italic := object.Weight, object.Italic
|
||||
if defaults != nil {
|
||||
if weight == 0 {
|
||||
weight = defaults.Weight
|
||||
}
|
||||
italic = italic || defaults.Italic
|
||||
}
|
||||
bold := weight >= 700
|
||||
if definition := r.Reader.fontCache[id]; definition != nil {
|
||||
bold = bold && !definition.Bold
|
||||
if definition.FontFile == "" && fontNoSyntheticBold(definition.FontName, definition.FamilyName) {
|
||||
bold = false
|
||||
}
|
||||
italic = italic && !definition.Italic
|
||||
}
|
||||
ascent, descent, _ := metrics.VerticalMetrics()
|
||||
unit := size / float64(metrics.UnitsPerEm())
|
||||
transforms := r.textObjectGlyphTransforms(id, object)
|
||||
codeOffset, textOffset := 0, 0
|
||||
angle := float64(object.CharDirection) * math.Pi / 180
|
||||
rotate := Matrix{a: math.Cos(angle), b: math.Sin(angle), c: -math.Sin(angle), d: math.Cos(angle)}
|
||||
glyphLinear := Matrix{a: linear.a, b: linear.b, c: linear.c, d: linear.d}.Multiply(rotate)
|
||||
for index, code := range object.TextCode {
|
||||
if object.textCodeLineBreak(index) {
|
||||
textOffset++
|
||||
}
|
||||
runes := textCodeRunes(code.Value)
|
||||
glyphs := textCodeGlyphs(runes, transforms, codeOffset)
|
||||
spans := textGlyphSpans(runes, transforms, codeOffset)
|
||||
if code.Index != "" {
|
||||
runes = r.parseIndexRunes(code.Index, id)
|
||||
glyphs = textRuneGlyphs(runes)
|
||||
spans = make([][2]int, len(glyphs))
|
||||
for i := range spans {
|
||||
spans[i] = [2]int{0, 1}
|
||||
}
|
||||
}
|
||||
positioner := NewTextPositioner(code, object.ReadDirection)
|
||||
for i, glyph := range glyphs {
|
||||
gid := uint16(glyph.GlyphID)
|
||||
if glyph.GlyphID < 0 {
|
||||
characters := []rune(glyph.Text)
|
||||
if len(characters) > 0 {
|
||||
gid = metrics.GlyphIndex(characters[0])
|
||||
}
|
||||
}
|
||||
path, err := r.preparedOutline(prepared, metrics, gid, size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
width := float64(metrics.GlyphAdvance(gid)) * unit
|
||||
advance := width * horizontal
|
||||
if (object.ReadDirection-object.CharDirection)%180 != 0 {
|
||||
advance = size
|
||||
}
|
||||
origin := positioner.Next(advance)
|
||||
scaleX := horizontal
|
||||
if object.CharDirection == 0 {
|
||||
limit := textGlyphAdvanceLimit(positioner.dxs, positioner.dys, positioner.xs, i, len(glyphs), origin.X)
|
||||
if limit > 0 && width*scaleX > limit {
|
||||
scaleX = limit / width
|
||||
}
|
||||
}
|
||||
if bold && len(path) > 0 {
|
||||
outline, err := geometry.Stroke(path, StrokeOptions{Width: size * 0.04, Join: "Round", Cap: "Round"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path, err = geometry.Combine(path, outline, GeometryUnion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if italic {
|
||||
path, err = geometry.Transform(path, Matrix{a: 1, c: -0.3, d: 1})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
x, y := matrix.Transform(origin.X, origin.Y)
|
||||
transform := TranslationMatrix(x, y).Multiply(glyphLinear).Multiply(Matrix{a: scaleX, d: 1})
|
||||
bounds := Box{Y: -float64(ascent) * unit, W: width, H: (float64(ascent) + float64(descent)) * unit}
|
||||
if len(path) > 0 {
|
||||
shape, err := geometry.Bounds(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bounds = unionTextBox(bounds, shape)
|
||||
}
|
||||
placed, err := geometry.Transform(path, transform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clipped, err := clipGeometry(geometry, placed, result.Clip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
box, err := geometry.Bounds(clipped)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
span := spans[i]
|
||||
item := PositionedGlyph{Path: placed, Start: textOffset + span[0], End: textOffset + span[1]}
|
||||
if strings.Contains(object.Decoration, "Underline") {
|
||||
line := GeometryPath{{Verb: GeometryMove, End: Point{Y: size * 0.1}}, {Verb: GeometryLine, End: Point{X: width * scaleX, Y: size * 0.1}}}
|
||||
line, err = geometry.Stroke(line, StrokeOptions{Width: size * 0.05})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.Underline, err = geometry.Transform(line, TranslationMatrix(x, y).Multiply(glyphLinear))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for j := item.Start; j < item.End; j++ {
|
||||
result.Run.Boxes[j] = unionTextBox(result.Run.Boxes[j], box)
|
||||
}
|
||||
if err := result.Run.addGeometrySpan(geometry, item.Start, item.End, bounds, transform, result.Clip); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result.Glyphs = append(result.Glyphs, item)
|
||||
}
|
||||
codeOffset += len(runes)
|
||||
if code.Index == "" {
|
||||
textOffset += len(runes)
|
||||
} else {
|
||||
textOffset++
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// addGeometrySpan 以公共几何语义收集文字选择区域,保留旋转和斜切
|
||||
// 入参: geometry 几何后端, start 起始字符, end 结束字符, bounds 局部范围, matrix 页面变换, clip 页面裁剪
|
||||
// 返回: error 几何错误
|
||||
func (run *TextRun) addGeometrySpan(geometry GeometryBackend, start, end int, bounds Box, matrix Matrix, clip *GeometryPath) error {
|
||||
inverse, ok := matrix.Invert()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if clip != nil {
|
||||
path, err := geometry.Transform(geometryRectangle(bounds), matrix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path, err = clipGeometry(geometry, path, clip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(path) == 0 {
|
||||
return nil
|
||||
}
|
||||
path, err = geometry.Transform(path, inverse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bounds, err = geometry.Bounds(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if bounds.W < 0 || bounds.H <= 0 {
|
||||
return nil
|
||||
}
|
||||
matrix = matrix.Multiply(TranslationMatrix(bounds.X, bounds.Y)).Multiply(Matrix{a: bounds.W, d: bounds.H})
|
||||
if n := len(run.Spans); n > 0 && run.Spans[n-1].Start == start && run.Spans[n-1].End == end {
|
||||
previous := MatrixFromValues(run.Spans[n-1].Matrix)
|
||||
if inverse, ok := previous.Invert(); ok {
|
||||
box := unionTextBox(Box{W: 1, H: 1}, inverse.Multiply(matrix).TransformBox(Box{W: 1, H: 1}))
|
||||
matrix = previous.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(Matrix{a: box.W, d: box.H})
|
||||
}
|
||||
run.Spans = run.Spans[:n-1]
|
||||
}
|
||||
run.Spans = append(run.Spans, TextSpan{Start: start, End: end, Matrix: matrix.Values()})
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user