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