feat(渲染编辑): 规范文字定位与注解度量
build.yaml / build (push) Successful in 48s

This commit is contained in:
xiaoqidun committed 2026-10-09 21:45:07 +08:00
1 parent 9b38f6f039
commit 444c01b976
6 files changed
+42 -42

No files matched your search

+1 -1
View File
@@ -1391,7 +1391,7 @@ export class CanvasEditor {
right = Math.max(right, x + advance * source.scale);
bottom = Math.max(bottom, y + size - baseline);
naturalX += advance;
x += run.dx?.length ? run.dx[Math.min(index, run.dx.length - 1)] * PX_PER_MM : run.dy?.length ? 0 : advance * source.scale;
x += run.dx?.length ? run.dx[Math.min(index, run.dx.length - 1)] * PX_PER_MM : 0;
y += run.dy?.length ? run.dy[Math.min(index, run.dy.length - 1)] * PX_PER_MM : 0;
index++;
}
+1 -1
View File
@@ -229,7 +229,7 @@ func (e *Editor) RewriteText(obj *TextObject, value string) error {
unit *= obj.HScale
}
advance := func(char rune) float64 { return float64(sfnt.GlyphAdvance(sfnt.GlyphIndex(char))) * unit }
if codes[i].DeltaX != "" || codes[i].DeltaY == "" {
if codes[i].DeltaX != "" {
codes[i].DeltaX = rewriteTextDeltas(codes[i].DeltaX, runes, replacement, from, to, advance)
}
if codes[i].DeltaY != "" {
+30 -4
View File
@@ -22,6 +22,12 @@ type ObjectContour struct {
EvenOdd bool `json:"evenOdd,omitempty"`
}
// annotationMeasurement 按注解绘制顺序收集对象范围和轮廓
type annotationMeasurement struct {
renderer *Renderer
result ObjectMeasurement
}
// ObjectBounds 获取文字、路径、图片或复合对象在页面坐标中的轴对齐范围,不修改对象
// 文字采用字形范围,路径包含描边与裁剪,图片采用裁剪后的几何范围,不解码像素或排除透明像素
// 底纹按填充或描边轮廓度量,不展开图案单元
@@ -38,11 +44,11 @@ func (r *Renderer) ObjectBounds(object GraphicObject, drawParam string) (Box, er
// 入参: annotation 注解
// 返回: Box 页面范围, []ObjectContour 命中轮廓, error 错误信息
func (r *Renderer) AnnotationGeometry(annotation Annotation) (Box, []ObjectContour, error) {
object := GraphicObject{Type: "CompositeObject", CompositeGraphicUnit: CompositeGraphicUnit{Boundary: annotation.Appearance.Boundary, Objects: annotation.Appearance.Objects}}
box, contours, err := r.ObjectGeometry(object, "")
if err != nil {
return box, contours, err
measurement := &annotationMeasurement{renderer: r}
if err := r.walkAnnotation(annotation, measurement); err != nil {
return Box{}, nil, err
}
box, contours := measurement.result.Bounds, measurement.result.Contours
for _, source := range r.annotationActionSources([]Annotation{annotation}) {
for _, action := range source.Actions {
if action.Event != "CLICK" {
@@ -65,6 +71,26 @@ func (r *Renderer) AnnotationGeometry(annotation Annotation) (Box, []ObjectConto
return box, contours, nil
}
// DrawObject 使用对象实际继承状态度量注解内容,不附加容器裁剪
// 入参: object 图形对象, state 绘制状态
// 返回: error 度量错误
func (m *annotationMeasurement) DrawObject(object *GraphicObject, state RenderState) error {
result, err := m.renderer.MeasureObject(*object, MeasureOptions{Defaults: state.Defaults, Parent: state.Parent, BoundaryInCTM: state.BoundaryInCTM, Clip: state.Clip, Contours: true})
if err != nil {
return err
}
m.result.Bounds = unionTextBox(m.result.Bounds, result.Bounds)
m.result.Contours = append(m.result.Contours, result.Contours...)
return nil
}
// DrawStamp 满足页面访问接口,注解外观遍历不包含数字签章
// 入参: stamp 数字签章
// 返回: error 始终为空
func (m *annotationMeasurement) DrawStamp(stamp Stamp) error {
return nil
}
// ObjectContours 获取路径、图片或复合对象的实际绘制区域,包含描边、虚线、变换及对象裁剪
// 图片不解码像素,底纹不展开图案单元;不包含页面边界或父级变换
// 入参: object 路径、图片或复合对象, drawParam 图层绘制参数标识
+2 -6
View File
@@ -171,15 +171,11 @@ func (r *Renderer) PositionText(object TextObject, state RenderState) (*Position
if err != nil {
return nil, err
}
positioner := NewTextPositioner(code, object.ReadDirection)
positioner := NewTextPositioner(code)
for i, gid := range ids {
path := paths[i]
width := float64(metrics.GlyphAdvance(gid)) * unit
advance := width * horizontal
if (object.ReadDirection-object.CharDirection)%180 != 0 {
advance = size
}
origin := positioner.Next(advance)
origin := positioner.Next()
scaleX := horizontal
if bold && len(path) > 0 {
outline, err := geometry.Stroke(path, StrokeOptions{Width: size * 0.04, Join: "Round", Cap: "Round"})
+2 -7
View File
@@ -203,7 +203,6 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
glyphTransforms := r.textObjectGlyphTransforms(fontID, obj)
hasUnderline := strings.Contains(obj.Decoration, "Underline")
_, shadedFill := fillPaint.(canvas.Gradient)
verticalAdvance := (obj.ReadDirection-obj.CharDirection)%180 != 0
codePos := 0
textPos := 0
for index, tc := range obj.TextCode {
@@ -223,7 +222,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
if textRun != nil && tc.Index == "" {
spans = textGlyphSpans(runes, glyphTransforms, codePos)
}
positioner := NewTextPositioner(tc, obj.ReadDirection)
positioner := NewTextPositioner(tc)
xs, ys := positioner.xs, positioner.ys
drawAsPath := embeddedFont || face.FauxBold > 0 || textCodePositioned(tc, xs, ys) || fillClip != nil || shadedFill || fillPattern != nil || shouldStroke
for i, glyph := range glyphs {
@@ -244,11 +243,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
} else {
glyphWidth = textGlyphWidth(face, glyph)
}
advance := glyphWidth * hScale
if verticalAdvance {
advance = sizeMM
}
position := positioner.Next(advance)
position := positioner.Next()
cx, cy := position.X, position.Y
var canvasX, canvasY float64
if boundaryInCTM && parentCTM != nil {
+6 -23
View File
@@ -19,9 +19,7 @@ import "slices"
// TextPositioner 按OFD坐标及增量定位连续字形,不依赖字体或绘图库
type TextPositioner struct {
xs, ys, dxs, dys []float64
direction Point
current Point
previous float64
index int
}
@@ -54,34 +52,22 @@ func textCodeOrigins(codes []TextCode, ascent float64) []TextCode {
return codes
}
// NewTextPositioner 创建单个TextCode的定位器,方向采用OFD角度
// 入参: code 文本定位数据, direction 阅读方向
// NewTextPositioner 创建单个TextCode的定位器,缺省位移不改变字形原点
// 入参: code 文本定位数据
// 返回: TextPositioner 字形定位器
func NewTextPositioner(code TextCode, direction int) TextPositioner {
result := TextPositioner{xs: parseFloats(code.X), ys: parseFloats(code.Y), dxs: parseFloats(code.DeltaX), dys: parseFloats(code.DeltaY), direction: Point{X: 1}}
switch direction {
case 90:
result.direction = Point{Y: 1}
case 180:
result.direction = Point{X: -1}
case 270:
result.direction = Point{Y: -1}
}
return result
func NewTextPositioner(code TextCode) TextPositioner {
return TextPositioner{xs: parseFloats(code.X), ys: parseFloats(code.Y), dxs: parseFloats(code.DeltaX), dys: parseFloats(code.DeltaY)}
}
// Next 返回下一个字形位置并保存其推进长度,显式坐标和增量优先
// 入参: advance 当前字形推进长度,单位为毫米
// Next 根据坐标与位移返回下一个字形原点
// 返回: Point 字形原点
func (p *TextPositioner) Next(advance float64) Point {
func (p *TextPositioner) Next() Point {
i := p.index
if i < len(p.xs) {
p.current.X = p.xs[i]
} else if i > 0 {
if dx, ok := textDelta(p.dxs, i-1); ok {
p.current.X += dx
} else if len(p.dys) == 0 {
p.current.X += p.previous * p.direction.X
}
}
if i < len(p.ys) {
@@ -89,11 +75,8 @@ func (p *TextPositioner) Next(advance float64) Point {
} else if i > 0 {
if dy, ok := textDelta(p.dys, i-1); ok {
p.current.Y += dy
} else if len(p.dxs) == 0 {
p.current.Y += p.previous * p.direction.Y
}
}
p.previous = advance
p.index++
return p.current
}