From c15547f0d201b2d5e3ae6d95ef9b65ea75e68277 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Wed, 23 Sep 2026 17:20:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=90=8E=E7=AB=AF=E7=BB=93=E6=9E=84):=20?= =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=A4=9A=E5=90=8E=E7=AB=AF=E8=A7=A3=E8=80=A6?= =?UTF-8?q?=EF=BC=8C=E6=B8=85=E7=90=86=E9=81=97=E7=95=99=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ofdgo_font_backend.go | 53 ++++++ ofdgo_image.go | 16 +- ofdgo_image_resource.go | 159 ++++++++++++++++ ofdgo_paint.go | 23 ++- ofdgo_paint_model.go | 87 +++++++++ ofdgo_raster.go | 8 +- ofdgo_render_scene.go | 157 +++++++++++++++ ofdgo_render_walk.go | 273 +++++++++++++++++++++++++++ ofdgo_renderer.go | 3 +- ofdgo_renderer_clip_canvas.go | 36 +++- ofdgo_renderer_compile_canvas.go | 24 ++- ofdgo_renderer_font_canvas.go | 43 +---- ofdgo_renderer_font_canvas_native.go | 2 +- ofdgo_renderer_font_loader_canvas.go | 52 ++--- ofdgo_renderer_geom_canvas.go | 50 +++++ ofdgo_renderer_glyph_canvas.go | 4 +- ofdgo_renderer_image_canvas.go | 88 +++------ ofdgo_renderer_layer_canvas.go | 228 +++++++++------------- ofdgo_renderer_page_canvas.go | 23 +-- ofdgo_renderer_paint_canvas.go | 92 +++------ ofdgo_renderer_path_canvas.go | 17 +- ofdgo_renderer_stamp_canvas.go | 2 +- ofdgo_renderer_state_canvas.go | 58 ++++++ ofdgo_renderer_svg_canvas.go | 4 +- ofdgo_renderer_text_canvas.go | 53 +----- ofdgo_text.go | 8 +- ofdgo_text_position.go | 68 +++++++ 27 files changed, 1166 insertions(+), 465 deletions(-) create mode 100644 ofdgo_image_resource.go create mode 100644 ofdgo_paint_model.go create mode 100644 ofdgo_render_scene.go create mode 100644 ofdgo_render_walk.go create mode 100644 ofdgo_renderer_state_canvas.go create mode 100644 ofdgo_text_position.go diff --git a/ofdgo_font_backend.go b/ofdgo_font_backend.go index 12a8d4a..fa10d00 100644 --- a/ofdgo_font_backend.go +++ b/ofdgo_font_backend.go @@ -57,6 +57,13 @@ type resolvedFontResult struct { err error } +// resetFontCache 重置字体解析及后端私有缓存,不预先创建任何绘图库对象 +func (r *Renderer) resetFontCache() { + r.preparedFonts = make(map[string]*PreparedFont) + r.resolvedFonts = make(map[resolvedFontKey]resolvedFontResult) + r.backendStates = make(map[any]any) +} + // ResolveFont 使用配置的字体后端解析资源,保留精确匹配与回退的区别 // 入参: id 字体资源标识, exact 是否要求精确匹配 // 返回: ResolvedFont 字体与来源, error 解析错误 @@ -73,3 +80,49 @@ func (r *Renderer) ResolveFont(id string, exact bool) (ResolvedFont, error) { r.resolvedFonts[key] = resolvedFontResult{font: resolved, err: err} return resolved, err } + +// PreparedFont 保存绘制用字体及GID、CID到包装字符的映射,所有数据只读 +// Data可与源编码不同,原始资源仍由ResolveFont和Reader保留 +type PreparedFont struct { + ResolvedFont + Glyphs map[uint16]rune + CIDs map[uint16]rune +} + +// PrepareFont 统一处理内嵌字体包装和索引映射,供各编译器复用 +// 入参: id 字体资源标识 +// 返回: *PreparedFont 绘制字体, error 字体解析错误 +func (r *Renderer) PrepareFont(id string) (*PreparedFont, error) { + if cached := r.preparedFonts[id]; cached != nil { + return cached, nil + } + resolved, err := r.ResolveFont(id, false) + if err != nil { + return nil, err + } + prepared := &PreparedFont{ResolvedFont: resolved} + definition := r.Reader.fontCache[id] + if definition != nil && definition.FontFile != "" { + prepared.CIDs = getCFFCIDRuneMap(resolved.Data) + if _, data, mapping, _, err := FixFontDataAggressive(resolved.Data, true, true); err == nil { + prepared.Data = data + if mapping != nil { + prepared.Glyphs = make(map[uint16]rune) + for character, glyph := range mapping { + if character == packedGlyphRune(glyph) { + prepared.Glyphs[glyph] = character + } + } + for character, glyph := range mapping { + if _, ok := prepared.Glyphs[glyph]; !ok { + prepared.Glyphs[glyph] = character + } + } + } + } + } + if definition != nil { + r.preparedFonts[id] = prepared + } + return prepared, nil +} diff --git a/ofdgo_image.go b/ofdgo_image.go index 8fcef57..27e090d 100644 --- a/ofdgo_image.go +++ b/ofdgo_image.go @@ -23,7 +23,6 @@ import ( _ "image/gif" "math" - canvasimage "github.com/tdewolff/canvas/image" _ "github.com/xiaoqidun/jbig2" _ "golang.org/x/image/bmp" "golang.org/x/image/draw" @@ -34,15 +33,12 @@ import ( // 入参: data 图片数据 // 返回: image.Image 图片对象, string 图片格式, error 错误信息 func decodeImageData(data []byte) (image.Image, string, error) { - if isJPEGData(data) { - if img, err := canvasimage.NewJPEGImage(bytes.NewReader(data)); err == nil { - return img, "jpeg", nil - } - } - if isPNGData(data) { - if img, err := canvasimage.NewPNGImage(bytes.NewReader(data)); err == nil { - return img, "png", nil + if isJPEGData(data) || isPNGData(data) { + img, err := newEncodedImage(data) + if err != nil { + return nil, "", err } + return img, img.format, nil } img, format, err := image.Decode(bytes.NewReader(data)) if err == nil { @@ -412,7 +408,7 @@ func imageWithTransparentEdge(img image.Image) (image.Image, int) { if w == 0 || h == 0 { return img, 0 } - if src, ok := img.(*canvasimage.Image); ok && src.Mimetype == "image/jpeg" && src.Mask == nil { + if src, ok := img.(*EncodedImage); ok && src.MIME() == "image/jpeg" { return img, 0 } source := imagePixelSource(img) diff --git a/ofdgo_image_resource.go b/ofdgo_image_resource.go new file mode 100644 index 0000000..4bdd997 --- /dev/null +++ b/ofdgo_image_resource.go @@ -0,0 +1,159 @@ +// 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 ( + "bytes" + "fmt" + "image" + "image/color" + _ "image/jpeg" + _ "image/png" + "io" + "sync" +) + +// EncodedImage 保留PNG或JPEG原始编码,首次读取像素时解码 +// Bytes返回只读数据,Image返回共享只读像素,解码错误由Image返回 +type EncodedImage struct { + data []byte + format string + config image.Config + once sync.Once + decoded image.Image + err error +} + +// NewEncodedImage 读取PNG或JPEG头信息,不解码像素,复制输入数据 +// 入参: data 原始编码 +// 返回: *EncodedImage 惰性图片, error 头信息或格式错误 +func NewEncodedImage(data []byte) (*EncodedImage, error) { + return newEncodedImage(bytes.Clone(data)) +} + +// newEncodedImage 接管编码数据并读取头信息 +// 入参: data 原始编码 +// 返回: *EncodedImage 惰性图片, error 头信息或格式错误 +func newEncodedImage(data []byte) (*EncodedImage, error) { + config, format, err := image.DecodeConfig(bytes.NewReader(data)) + if err != nil { + return nil, err + } + if format != "jpeg" && format != "png" { + return nil, fmt.Errorf("unsupported encoded image %q", format) + } + return &EncodedImage{data: data, format: format, config: config}, nil +} + +// Bytes 返回只读原始编码,调用方不得修改 +// 返回: []byte 原始编码 +func (img *EncodedImage) Bytes() []byte { return img.data } + +// MIME 返回图片媒体类型 +// 返回: string 媒体类型 +func (img *EncodedImage) MIME() string { return "image/" + img.format } + +// ColorModel 返回头信息中的颜色模型,不解码像素 +// 返回: color.Model 颜色模型 +func (img *EncodedImage) ColorModel() color.Model { return img.config.ColorModel } + +// Bounds 返回图片范围,不解码像素 +// 返回: image.Rectangle 图片范围 +func (img *EncodedImage) Bounds() image.Rectangle { + return image.Rect(0, 0, img.config.Width, img.config.Height) +} + +// Image 首次解码图片并缓存结果或错误,支持并发读取 +// 返回: image.Image 只读像素, error 解码错误 +func (img *EncodedImage) Image() (image.Image, error) { + img.once.Do(func() { img.decoded, _, img.err = image.Decode(bytes.NewReader(img.data)) }) + return img.decoded, img.err +} + +// At 返回像素颜色,解码失败时返回透明色,绘制前应通过Image检查错误 +// 入参: x 横坐标, y 纵坐标 +// 返回: color.Color 像素颜色 +func (img *EncodedImage) At(x, y int) color.Color { + decoded, err := img.Image() + if err != nil { + return color.Transparent + } + return decoded.At(x, y) +} + +// ImageResource 按资源标识读取只读图片,PNG和JPEG保留原始编码并惰性解码 +// 应先读取源页面以加载页面资源,返回EncodedImage时通过Image检查像素解码错误 +// 入参: id 图片资源标识 +// 返回: image.Image 图片资源, error 读取错误 +func (r *Renderer) ImageResource(id string) (image.Image, error) { + path, ok := r.Reader.ResMap[id] + if !ok { + return nil, fmt.Errorf("image resource %q not found", id) + } + return r.cachedImageResource(path) +} + +// cachedImageResource 复用原始图片资源,不依赖输出后端 +// 入参: resPath 图片资源路径 +// 返回: image.Image 图片对象, error 读取错误 +func (r *Renderer) cachedImageResource(resPath string) (image.Image, error) { + resPath = cleanPackagePath(r.Reader.ResPath(resPath)) + img, ok := r.imageCache[resPath] + if !ok { + var err error + img, err = r.readImageResource(resPath) + if err != nil { + return nil, err + } + if r.imageCache == nil { + r.imageCache = make(map[string]image.Image) + } + r.imageCache[resPath] = img + } + return img, nil +} + +// decodeImageResource 读取或复用图片资源,按输出需要解码像素 +// 入参: resPath 图片资源路径 +// 返回: image.Image 图片对象, error 读取或解码错误 +func (r *Renderer) decodeImageResource(resPath string) (image.Image, error) { + img, err := r.cachedImageResource(resPath) + if err != nil { + return nil, err + } + if r.decodeImages { + if source, ok := img.(*EncodedImage); ok { + return source.Image() + } + } + return img, nil +} + +// readImageResource 读取图片资源并保留可直接嵌入的原始编码 +// 入参: resPath 图片资源路径 +// 返回: image.Image 图片对象, error 读取或解码错误 +func (r *Renderer) readImageResource(resPath string) (image.Image, error) { + rc, err := r.Reader.openFile(resPath) + if err != nil { + return nil, err + } + defer rc.Close() + data, err := io.ReadAll(rc) + if err != nil { + return nil, err + } + img, _, err := decodeImageData(data) + return img, err +} diff --git a/ofdgo_paint.go b/ofdgo_paint.go index b5d13e6..5484fbe 100644 --- a/ofdgo_paint.go +++ b/ofdgo_paint.go @@ -191,21 +191,21 @@ func (r *Renderer) parseStrokeColor(strokeColor *StrokeColor) color.Color { return r.parseFillColor((*FillColor)(strokeColor)) } -// patternPaint 底纹画刷 -type patternPaint struct { +// PatternPaint 底纹画刷 +type PatternPaint struct { *Pattern - color FillColor - alpha *int + Color FillColor + Alpha *int } // parsePatternPaint 解析底纹画刷 // 入参: fill 填充颜色节点 -// 返回: *patternPaint 底纹画刷 -func parsePatternPaint(fill *FillColor) *patternPaint { +// 返回: *PatternPaint 底纹画刷 +func parsePatternPaint(fill *FillColor) *PatternPaint { if fill.Pattern == nil { return nil } - return &patternPaint{Pattern: fill.Pattern, color: FillColor{Value: fill.Value, Index: fill.Index, ColorSpace: fill.ColorSpace}, alpha: fill.Alpha} + return &PatternPaint{Pattern: fill.Pattern, Color: FillColor{Value: fill.Value, Index: fill.Index, ColorSpace: fill.ColorSpace}, Alpha: fill.Alpha} } // parseShdColor 解析渐变颜色 @@ -219,12 +219,11 @@ func (r *Renderer) parseShdColor(segments []ShdSegment, alpha *int) color.Color return r.parseColorWithAlpha(c.Value, c.Index, c.ColorSpace, mergeAlpha(c.Alpha, alpha)) } -// GradientStops 解析OFD渐变分段的位置与透明度,返回独立数据并保留原分段顺序 // GradientStops 解析OFD渐变分段的位置与透明度,返回独立数据并保留原分段顺序 // 入参: segments 渐变分段, alpha 透明度 -// 返回: []RasterStop 后端无关的渐变分段 -func (r *Renderer) GradientStops(segments []ShdSegment, alpha *int) []RasterStop { - var gradient []RasterStop +// 返回: []ColorStop 后端无关的渐变分段 +func (r *Renderer) GradientStops(segments []ShdSegment, alpha *int) []ColorStop { + var gradient []ColorStop position, step := 0.0, 0.0 for i, segment := range segments { if !segment.positionMissing { @@ -246,7 +245,7 @@ func (r *Renderer) GradientStops(segments []ShdSegment, alpha *int) []RasterStop offset := position position += step segmentAlpha := mergeAlpha(segment.Color.Alpha, alpha) - gradient = append(gradient, RasterStop{Offset: offset, Color: r.ResolveColor(segment.Color.Value, segment.Color.Index, segment.Color.ColorSpace, segmentAlpha)}) + gradient = append(gradient, ColorStop{Offset: offset, Color: r.ResolveColor(segment.Color.Value, segment.Color.Index, segment.Color.ColorSpace, segmentAlpha)}) } if len(gradient) == 0 { return nil diff --git a/ofdgo_paint_model.go b/ofdgo_paint_model.go new file mode 100644 index 0000000..07544cd --- /dev/null +++ b/ofdgo_paint_model.go @@ -0,0 +1,87 @@ +// 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 "image/color" + +// ColorStop 保存预乘RGBA颜色节点及渐变区间位置 +type ColorStop struct { + Offset float64 + Color color.RGBA +} + +// PaintKind 区分无画刷、纯色、轴向渐变、径向渐变和图案 +type PaintKind uint8 + +const ( + PaintNone PaintKind = iota + PaintSolid + PaintLinear + PaintRadial + PaintPattern +) + +// Paint 保存后端无关的颜色、渐变和图案,不修改源节点 +type Paint struct { + Kind PaintKind + Color color.RGBA + Gradient *Shading + Pattern *PatternPaint +} + +// Shading 保存OFD对象局部坐标中的渐变,单位为毫米,纵轴向下 +// Extend、MapType和MapUnit保留标准的延伸及重复语义 +type Shading struct { + Start, End Point + StartRadius, EndRadius float64 + Stops []ColorStop + Extend, MapType string + MapUnit, Eccentricity, Angle float64 +} + +// ResolvePaint 解析颜色、透明度、渐变分段和图案,不依赖绘图库 +// 入参: fill 填充或转换后的描边节点 +// 返回: Paint 只读画刷 +func (r *Renderer) ResolvePaint(fill *FillColor) Paint { + if fill == nil { + return Paint{} + } + if fill.Pattern != nil { + return Paint{Kind: PaintPattern, Pattern: parsePatternPaint(fill)} + } + base := r.parseFillColor(fill) + if base == nil { + return Paint{} + } + paint := Paint{Kind: PaintSolid, Color: colorToRGBA(base)} + if node := fill.AxialShd; node != nil { + start, end := parseFloats(node.StartPoint), parseFloats(node.EndPoint) + stops := r.GradientStops(node.Segment, fill.Alpha) + if len(start) >= 2 && len(end) >= 2 && len(stops) != 0 && (!geometryEqual(start[0], end[0]) || !geometryEqual(start[1], end[1])) { + paint.Kind = PaintLinear + paint.Gradient = &Shading{Start: Point{X: start[0], Y: start[1]}, End: Point{X: end[0], Y: end[1]}, Stops: stops, Extend: node.Extend, MapType: node.MapType, MapUnit: node.MapUnit} + return paint + } + } + if node := fill.RadialShd; node != nil && node.EndRadius > 0 { + start, end := parseFloats(node.StartPoint), parseFloats(node.EndPoint) + stops := r.GradientStops(node.Segment, fill.Alpha) + if len(start) >= 2 && len(end) >= 2 && len(stops) != 0 { + paint.Kind = PaintRadial + paint.Gradient = &Shading{Start: Point{X: start[0], Y: start[1]}, End: Point{X: end[0], Y: end[1]}, StartRadius: node.StartRadius, EndRadius: node.EndRadius, Stops: stops, Extend: node.Extend, MapType: node.MapType, MapUnit: node.MapUnit, Eccentricity: node.Eccentricity, Angle: node.Angle} + } + } + return paint +} diff --git a/ofdgo_raster.go b/ofdgo_raster.go index 8cd6f63..ce1c310 100644 --- a/ofdgo_raster.go +++ b/ofdgo_raster.go @@ -79,18 +79,12 @@ const ( RasterRadial ) -// RasterStop 按Offset升序排列的预乘RGBA颜色节点 -type RasterStop struct { - Offset float64 - Color color.RGBA -} - // RasterGradient 局部坐标下的线性或双圆径向渐变,区间外延续边界颜色 type RasterGradient struct { Kind RasterGradientKind Start, End RasterPoint R0, R1 float64 - Stops []RasterStop + Stops []ColorStop } // PixelSize 检查页面尺寸并计算目标像素大小 diff --git a/ofdgo_render_scene.go b/ofdgo_render_scene.go new file mode 100644 index 0000000..7a4e5a3 --- /dev/null +++ b/ofdgo_render_scene.go @@ -0,0 +1,157 @@ +// 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 + +// PageScene 保存后端无关的页面语义与绘制顺序,不提前展开文字或重新编码图片 +// 场景借用源页面及资源的只读引用,编辑文档后应重新构建 +type PageScene struct { + Source *PageContent + Box Box + commands []sceneCommand +} + +// sceneCommand 保存叶子对象、签章或嵌套分组事件 +type sceneCommand struct { + kind sceneCommandKind + object *GraphicObject + state RenderState + stamp Stamp + annotation string + count int +} + +// sceneCommandKind 区分场景中的绘制和分组事件 +type sceneCommandKind uint8 + +const ( + sceneObject sceneCommandKind = iota + sceneStamp + sceneBeginObject + sceneBeginAnnotation + sceneEnd + sceneSkip +) + +// CompileScene 展开页面结构,保留原文、字形索引、动作和对象标识 +// 不提前绘制文字或图片,裁剪轮廓按几何后端需求解析 +// 入参: page 源页面 +// 返回: *PageScene 只读语义场景, error 遍历或几何错误 +func (r *Renderer) CompileScene(page *PageContent) (*PageScene, error) { + box, err := r.GetPageBox(page) + if err != nil { + return nil, err + } + scene := &PageScene{Source: page, Box: box} + if err := r.WalkPage(page, (*sceneRecorder)(scene)); err != nil { + return nil, err + } + return scene, nil +} + +// Walk 重放语义场景,访问器仍可自行处理文字和原始图片 +// 入参: visitor 页面访问器 +// 返回: error 访问器错误 +func (s *PageScene) Walk(visitor PageVisitor) error { + groups, grouped := visitor.(PageGroups) + var stack []bool + for _, command := range s.commands { + switch command.kind { + case sceneObject: + if err := visitor.DrawObject(command.object, command.state); err != nil { + closeSceneGroups(groups, stack) + return err + } + case sceneStamp: + if err := visitor.DrawStamp(command.stamp); err != nil { + closeSceneGroups(groups, stack) + return err + } + case sceneBeginObject: + if grouped { + stack = append(stack, groups.BeginObject(command.object)) + } + case sceneBeginAnnotation: + if grouped { + stack = append(stack, groups.BeginAnnotation(command.annotation)) + } + case sceneEnd: + if grouped { + if stack[len(stack)-1] { + groups.EndObject() + } + stack = stack[:len(stack)-1] + } + case sceneSkip: + if grouped { + groups.SkipObjects(command.count) + } + } + } + return nil +} + +// closeSceneGroups 在访问器失败时配对结束已建立的分组 +// 入参: groups 分组访问器, stack 分组状态 +func closeSceneGroups(groups PageGroups, stack []bool) { + for i := len(stack) - 1; i >= 0; i-- { + if stack[i] { + groups.EndObject() + } + } +} + +// sceneRecorder 收集公共遍历事件,不解释绘制外观 +type sceneRecorder PageScene + +// DrawObject 保留源对象及继承状态 +// 入参: object 源对象, state 继承状态 +// 返回: error 固定为空 +func (s *sceneRecorder) DrawObject(object *GraphicObject, state RenderState) error { + s.commands = append(s.commands, sceneCommand{kind: sceneObject, object: object, state: state}) + return nil +} + +// DrawStamp 保留签章外观 +// 入参: stamp 签章 +// 返回: error 固定为空 +func (s *sceneRecorder) DrawStamp(stamp Stamp) error { + s.commands = append(s.commands, sceneCommand{kind: sceneStamp, stamp: stamp}) + return nil +} + +// BeginObject 记录对象分组 +// 入参: object 源对象 +// 返回: bool 固定为true +func (s *sceneRecorder) BeginObject(object *GraphicObject) bool { + s.commands = append(s.commands, sceneCommand{kind: sceneBeginObject, object: object}) + return true +} + +// BeginAnnotation 记录注解分组 +// 入参: id 注解标识 +// 返回: bool 固定为true +func (s *sceneRecorder) BeginAnnotation(id string) bool { + s.commands = append(s.commands, sceneCommand{kind: sceneBeginAnnotation, annotation: id}) + return true +} + +// EndObject 记录分组结束 +func (s *sceneRecorder) EndObject() { s.commands = append(s.commands, sceneCommand{kind: sceneEnd}) } + +// SkipObjects 记录不可见成员数量,保持可编辑输出的对象编号 +// 入参: count 对象数量 +func (s *sceneRecorder) SkipObjects(count int) { + s.commands = append(s.commands, sceneCommand{kind: sceneSkip, count: count}) +} diff --git a/ofdgo_render_walk.go b/ofdgo_render_walk.go new file mode 100644 index 0000000..bd27b80 --- /dev/null +++ b/ofdgo_render_walk.go @@ -0,0 +1,273 @@ +// 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" + +// RenderState 保存对象继承的样式、父变换和页面坐标裁剪,所有引用只读 +// Clip为nil表示未裁剪,空路径表示完全裁去,单位为毫米 +type RenderState struct { + Defaults *DrawParam + Parent *Matrix + BoundaryInCTM bool + Clip *GeometryPath +} + +// PageVisitor 消费按标准图层顺序展开的对象与印章,源对象和状态只读 +// 保留引用时文档也须保持不变,坐标与属性继续遵循OFD语义 +// 复合图元的变换、透明度和引用由公共遍历处理,具体对象外观由访问器解释 +type PageVisitor interface { + DrawObject(object *GraphicObject, state RenderState) error + DrawStamp(stamp Stamp) error +} + +// PageGroups 提供可选的对象及注解分组,返回true时配对调用EndObject +// SkipObjects保持不可见复合图元的成员编号,供可编辑矢量输出使用 +type PageGroups interface { + BeginObject(object *GraphicObject) bool + BeginAnnotation(id string) bool + EndObject() + SkipObjects(count int) +} + +// WalkPage 按模板、图层、注解和印章顺序访问页面,不依赖任何绘图库 +// 页面背景由调用方处理,文字裁剪可通过几何后端加载字体,图片由访问器按需加载 +// 入参: page 页面内容, visitor 页面访问器 +// 返回: error 遍历或访问器错误 +func (r *Renderer) WalkPage(page *PageContent, visitor PageVisitor) error { + for order := range 3 { + if r.Reader.doc != nil { + for _, ref := range page.Template { + kind := ref.ZOrder + if kind == "" { + kind = "Background" + } + if layerOrder(kind) != order { + continue + } + template := r.loadTemplate(ref.TemplateID) + if template == nil { + continue + } + for templateOrder := range 3 { + if err := r.walkLayers(template.Content.Layer, templateOrder, visitor); err != nil { + return err + } + } + } + } + if err := r.walkLayers(page.Content.Layer, order, visitor); err != nil { + return err + } + } + if r.RenderAnnotations { + for _, annotation := range r.Reader.Annots[page.ID] { + if annotation.Visible != nil && !*annotation.Visible { + continue + } + if err := r.walkAnnotation(annotation, visitor); err != nil { + return err + } + } + } + for _, stamp := range r.Reader.Stamps[page.ID] { + if err := visitor.DrawStamp(stamp); err != nil { + return err + } + } + return nil +} + +// walkAnnotation 访问注解外观并保持独立分组和定位 +// 入参: annotation 注解, visitor 页面访问器 +// 返回: error 遍历或访问器错误 +func (r *Renderer) walkAnnotation(annotation Annotation, visitor PageVisitor) error { + if groups, ok := visitor.(PageGroups); ok && groups.BeginAnnotation(annotation.ID) { + defer groups.EndObject() + } + box, _ := ParseBox(annotation.Appearance.Boundary) + matrix := TranslationMatrix(box.X, box.Y) + state := RenderState{Parent: &matrix} + for i := range annotation.Appearance.Objects { + if err := r.WalkObject(&annotation.Appearance.Objects[i], state, visitor); err != nil { + return err + } + } + return nil +} + +// walkLayers 按原顺序访问指定层级,不重新排列同类图层 +// 入参: layers 图层列表, order 层级顺序, visitor 页面访问器 +// 返回: error 遍历或访问器错误 +func (r *Renderer) walkLayers(layers []Layer, order int, visitor PageVisitor) error { + for _, layer := range layers { + if layerOrder(layer.Type) != order { + continue + } + state := RenderState{Defaults: r.drawParamDefaults(layer.DrawParam, nil)} + if len(layer.Objects) != 0 { + for i := range layer.Objects { + if err := r.WalkObject(&layer.Objects[i], state, visitor); err != nil { + return err + } + } + continue + } + for _, object := range layer.TextObject { + if err := r.WalkObject(&GraphicObject{Type: "TextObject", TextObject: object}, state, visitor); err != nil { + return err + } + } + for _, object := range layer.PathObject { + if err := r.WalkObject(&GraphicObject{Type: "PathObject", PathObject: object}, state, visitor); err != nil { + return err + } + } + for _, object := range layer.ImageObject { + if err := r.WalkObject(&GraphicObject{Type: "ImageObject", ImageObject: object}, state, visitor); err != nil { + return err + } + } + for _, object := range layer.CompositeGraphicUnit { + if err := r.WalkObject(&GraphicObject{Type: "CompositeGraphicUnit", CompositeGraphicUnit: object}, state, visitor); err != nil { + return err + } + } + } + return nil +} + +// WalkObject 访问单个对象或展开复合图元,保留父级裁剪和源对象标识 +// 入参: object 源对象, state 继承状态, visitor 页面访问器 +// 返回: error 遍历或访问器错误 +func (r *Renderer) WalkObject(object *GraphicObject, state RenderState, visitor PageVisitor) error { + return r.walkObject(object, state, visitor, nil) +} + +// walkObject 访问对象并传递当前资源引用链 +// 入参: object 源对象, state 继承状态, visitor 页面访问器, references 当前资源引用链 +// 返回: error 遍历或访问器错误 +func (r *Renderer) walkObject(object *GraphicObject, state RenderState, visitor PageVisitor, references map[string]bool) error { + if groups, ok := visitor.(PageGroups); ok && groups.BeginObject(object) { + defer groups.EndObject() + } + switch object.Type { + case "TextObject", "PathObject", "ImageObject": + return visitor.DrawObject(object, state) + case "CompositeGraphicUnit", "CompositeObject": + return r.walkComposite(object.CompositeGraphicUnit, state, visitor, references) + default: + return fmt.Errorf("unsupported graphic object %q", object.Type) + } +} + +// walkComposite 展开复合图元的资源、成员与透明度,不修改共享资源 +// 入参: object 复合图元, state 继承状态, visitor 页面访问器, references 当前资源引用链 +// 返回: error 几何或访问器错误 +func (r *Renderer) walkComposite(object CompositeGraphicUnit, state RenderState, visitor PageVisitor, references map[string]bool) error { + if object.Visible != nil && !*object.Visible { + if groups, ok := visitor.(PageGroups); ok { + count, ref, seen := len(object.Objects), object.ResourceID, make(map[string]bool) + for ref != "" && !seen[ref] { + seen[ref] = true + unit := r.CompositeGraphicUnits[ref] + if unit == nil { + break + } + count, ref = count+len(unit.Objects), unit.ResourceID + } + groups.SkipObjects(count) + } + return nil + } + box, _ := ParseBox(object.Boundary) + boundary := TranslationMatrix(box.X, box.Y) + if state.Parent != nil { + boundary = state.Parent.Multiply(boundary) + } + matrix := boundary.Multiply(NewMatrix(object.CTM)) + clips, clipMatrix := object.Clips, matrix + if clips != nil && clips.TransFlag != nil && !*clips.TransFlag { + copy := *clips + copy.TransFlag = nil + clips, clipMatrix = ©, boundary + } + if clips != nil { + geometry, err := r.Geometry() + if err != nil { + return err + } + state.Clip, err = geometry.Clip(r, clips, clipMatrix, state.Clip) + if err != nil { + return err + } + } + state.Defaults = r.drawParamDefaults(object.DrawParam, state.Defaults) + state.Parent = &matrix + if object.ResourceID != "" { + if references[object.ResourceID] { + return fmt.Errorf("cyclic composite resource %q", object.ResourceID) + } + if ref, ok := r.CompositeGraphicUnits[object.ResourceID]; ok { + if references == nil { + references = make(map[string]bool) + } + references[object.ResourceID] = true + copy := *ref + copy.Alpha = mergeAlpha(copy.Alpha, object.Alpha) + referenceState := state + referenceState.BoundaryInCTM = true + err := r.walkComposite(copy, referenceState, visitor, references) + delete(references, object.ResourceID) + if err != nil { + return err + } + } + } + if len(object.Objects) != 0 { + for _, member := range object.Objects { + member = mergeGraphicObjectAlpha(member, object.Alpha) + if err := r.walkObject(&member, state, visitor, references); err != nil { + return err + } + } + return nil + } + for _, member := range object.ImageObject { + member.Alpha = mergeAlpha(member.Alpha, object.Alpha) + if err := visitor.DrawObject(&GraphicObject{Type: "ImageObject", ImageObject: member}, state); err != nil { + return err + } + } + for _, member := range object.PathObject { + member.Alpha = mergeAlpha(member.Alpha, object.Alpha) + if err := visitor.DrawObject(&GraphicObject{Type: "PathObject", PathObject: member}, state); err != nil { + return err + } + } + for _, member := range object.TextObject { + member.Alpha = mergeAlpha(member.Alpha, object.Alpha) + if err := visitor.DrawObject(&GraphicObject{Type: "TextObject", TextObject: member}, state); err != nil { + return err + } + } + for _, member := range object.CompositeGraphicUnit { + member.Alpha = mergeAlpha(member.Alpha, object.Alpha) + if err := r.walkComposite(member, state, visitor, references); err != nil { + return err + } + } + return nil +} diff --git a/ofdgo_renderer.go b/ofdgo_renderer.go index c4d5b2a..866aa95 100644 --- a/ofdgo_renderer.go +++ b/ofdgo_renderer.go @@ -25,7 +25,6 @@ import ( // OnExportProgress可选,同步回报文档导出的已处理页数及总页数,返回错误则停止,页数完成不代表写入成功 // TransparentBackground关闭页面白底,供嵌套图案和印章保持透明背景 type Renderer struct { - canvasFontState Reader *Reader DPI float64 RenderAnnotations bool @@ -41,7 +40,9 @@ type Renderer struct { pageText *PageText textOnly bool backends RenderBackends + backendStates map[any]any resolvedFonts map[resolvedFontKey]resolvedFontResult + preparedFonts map[string]*PreparedFont renderError error TransparentBackground bool } diff --git a/ofdgo_renderer_clip_canvas.go b/ofdgo_renderer_clip_canvas.go index 0a858f1..3ac4128 100644 --- a/ofdgo_renderer_clip_canvas.go +++ b/ofdgo_renderer_clip_canvas.go @@ -68,15 +68,39 @@ func (r *clipRenderer) RenderImage(img image.Image, m canvas.Matrix) {} // 入参: clips 裁剪对象, pageH 页面高度, bx 边界X坐标, by 边界Y坐标, objectCTM 对象CTM, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM // 返回: *canvas.Path 路径对象 func (r *Renderer) buildObjectClipPath(clips *Clips, pageH float64, bx, by float64, objectCTM Matrix, parentCTM *Matrix, boundaryInCTM bool) *canvas.Path { + if clips == nil { + return nil + } if !boundaryInCTM && parentCTM != nil { objectCTM = parentCTM.Multiply(objectCTM) } - p := r.buildClipPath(clips, pageH, bx, by, objectCTM) - if p != nil && boundaryInCTM && parentCTM != nil { - p = p.Transform(canvas.Matrix{ - {parentCTM.a, -parentCTM.c, parentCTM.c*pageH + parentCTM.e}, - {-parentCTM.b, parentCTM.d, pageH*(1-parentCTM.d) - parentCTM.f}, - }) + if clips.TransFlag != nil && !*clips.TransFlag { + copy := *clips + copy.TransFlag = nil + clips = © + objectCTM = NewMatrix("") + } + matrix := TranslationMatrix(bx, by).Multiply(objectCTM) + if boundaryInCTM && parentCTM != nil { + matrix = parentCTM.Multiply(matrix) + } + geometry, err := r.Geometry() + if err != nil { + r.renderError = err + return nil + } + path, err := geometry.Clip(r, clips, matrix, nil) + if err != nil { + r.renderError = err + return nil + } + p, err := geometryToCanvasPath(path) + if err != nil { + r.renderError = err + return nil + } + if p != nil { + p = p.Translate(0, pageH) } return p } diff --git a/ofdgo_renderer_compile_canvas.go b/ofdgo_renderer_compile_canvas.go index 0e2fab0..1368dfe 100644 --- a/ofdgo_renderer_compile_canvas.go +++ b/ofdgo_renderer_compile_canvas.go @@ -22,7 +22,7 @@ import ( "github.com/tdewolff/canvas" ) -// CompilePage 使用默认字体和几何解释器生成独立绘制数据 +// CompilePage 使用Canvas页面解释器和配置的字体、几何后端生成独立绘制数据 // 入参: r 渲染器, page 页面内容 // 返回: *RasterPage 绘制页面, error 编译错误 func (CanvasBackend) CompilePage(r *Renderer, page *PageContent) (*RasterPage, error) { @@ -34,7 +34,11 @@ func (CanvasBackend) CompilePage(r *Renderer, page *PageContent) (*RasterPage, e if _, _, err := result.PixelSize(); err != nil { return nil, err } - compiler := &canvasPageCompiler{page: result} + geometry, err := r.Geometry() + if err != nil { + return nil, err + } + compiler := &canvasPageCompiler{page: result, geometry: geometry} if err := r.renderCanvasContext(canvas.NewContext(compiler), page); err != nil { return nil, err } @@ -46,8 +50,9 @@ func (CanvasBackend) CompilePage(r *Renderer, page *PageContent) (*RasterPage, e // canvasPageCompiler 将已解释的页面绘制转换为自有数据,不保存第三方绘图对象 type canvasPageCompiler struct { - page *RasterPage - err error + page *RasterPage + geometry GeometryBackend + err error } // Size 返回物理页面尺寸 @@ -69,7 +74,12 @@ func (c *canvasPageCompiler) RenderPath(path *canvas.Path, style canvas.Style, m offset, dashes := canvas.ScaleDash(style.StrokeWidth, style.DashOffset, style.Dashes) stroke = stroke.Dash(offset, dashes...) } - stroke = stroke.Stroke(style.StrokeWidth, style.StrokeCapper, style.StrokeJoiner, canvas.PixelTolerance/(c.page.DPI/25.4)) + var err error + stroke, err = canvasStrokePath(c.geometry, stroke, canvasStrokeOptions(style.StrokeWidth, style.StrokeCapper, style.StrokeJoiner, canvas.PixelTolerance/(c.page.DPI/25.4))) + if err != nil { + c.err = err + return + } c.fill(stroke, style.Stroke, style.FillRule, m) } } @@ -100,9 +110,9 @@ func (c *canvasPageCompiler) fill(path *canvas.Path, paint canvas.Paint, rule ca c.err = fmt.Errorf("unsupported raster gradient %T", paint.Gradient) return } - g.Stops = make([]RasterStop, len(stops)) + g.Stops = make([]ColorStop, len(stops)) for i, stop := range stops { - g.Stops[i] = RasterStop{Offset: stop.Offset, Color: stop.Color} + g.Stops[i] = ColorStop{Offset: stop.Offset, Color: stop.Color} } cmd.Paint.Gradient = g } diff --git a/ofdgo_renderer_font_canvas.go b/ofdgo_renderer_font_canvas.go index 50eeb28..3f9a26c 100644 --- a/ofdgo_renderer_font_canvas.go +++ b/ofdgo_renderer_font_canvas.go @@ -54,7 +54,7 @@ func (CanvasBackend) ResolveFont(r *Renderer, id string, exact bool) (ResolvedFo if source, family := r.fontSourceMatch(id, definition, exact); family != nil { sfnt := family.Face(12, style).Font.SFNT face := fontFaceInfo(sfnt.Tables["name"], source.face) - r.fontSourceUsed[id] = source + r.canvasState().fontSourceUsed[id] = source return ResolvedFont{Data: fontSFNTData(sfnt), Source: source.name, Face: &face, Exact: source.exact}, nil } if !exact { @@ -66,11 +66,11 @@ func (CanvasBackend) ResolveFont(r *Renderer, id string, exact bool) (ResolvedFo // canvasFallbackFont 按需加载默认阅读字体,不用于精确编辑匹配 // 返回: ResolvedFont 默认字体,未安装时数据为空 func (r *Renderer) canvasFallbackFont() ResolvedFont { - if r.fontFamily == nil { + if r.canvasState().fontFamily == nil { r.initCanvasFonts() } - if r.defaultFontLoaded { - return ResolvedFont{Data: fontSFNTData(r.fontFamily.Face(12, canvas.FontRegular).Font.SFNT)} + if r.canvasState().defaultFontLoaded { + return ResolvedFont{Data: fontSFNTData(r.canvasState().fontFamily.Face(12, canvas.FontRegular).Font.SFNT)} } return ResolvedFont{} } @@ -97,39 +97,8 @@ func (f canvasFontMetrics) Write() []byte { return fontSFNTData(f.SFNT) } -// canvasFontState 保存默认适配器的字体与字形缓存,不向后端接口暴露绘图库类型 -type canvasFontState struct { - fontFamily *canvas.FontFamily - defaultFontLoaded bool - fontMap map[string]*canvas.FontFamily - fontGIDMap map[string]map[uint16]rune - fontCIDMap map[string]map[uint16]rune - fontCache map[fontCacheKey]*canvas.FontFamily - svgFontCache map[*canvas.Font]SVGFont - fontSourceCache map[string][]fontSource - fontSourceUsed map[string]fontSource - fontDirCandidates map[string][]fontFileCandidate - fontFSCandidates map[int][]fontFileCandidate - textGlyphPathCache map[textGlyphPathCacheKey]textGlyphPathCacheValue -} - -// resetFontCache 重置字体匹配、加载和字形缓存 -func (r *Renderer) resetFontCache() { - r.resolvedFonts = make(map[resolvedFontKey]resolvedFontResult) - r.fontMap = make(map[string]*canvas.FontFamily) - r.fontGIDMap = make(map[string]map[uint16]rune) - r.fontCIDMap = make(map[string]map[uint16]rune) - r.fontCache = make(map[fontCacheKey]*canvas.FontFamily) - r.svgFontCache = make(map[*canvas.Font]SVGFont) - r.fontSourceCache = make(map[string][]fontSource) - r.fontSourceUsed = make(map[string]fontSource) - r.fontDirCandidates = make(map[string][]fontFileCandidate) - r.fontFSCandidates = make(map[int][]fontFileCandidate) - r.textGlyphPathCache = make(map[textGlyphPathCacheKey]textGlyphPathCacheValue) -} - // initCanvasFonts 初始化Canvas默认字体 func (r *Renderer) initCanvasFonts() { - r.fontFamily = canvas.NewFontFamily("default") - r.defaultFontLoaded = r.loadDefaultFonts() + r.canvasState().fontFamily = canvas.NewFontFamily("default") + r.canvasState().defaultFontLoaded = r.loadDefaultFonts() } diff --git a/ofdgo_renderer_font_canvas_native.go b/ofdgo_renderer_font_canvas_native.go index 2a8039b..5ef1b1e 100644 --- a/ofdgo_renderer_font_canvas_native.go +++ b/ofdgo_renderer_font_canvas_native.go @@ -22,7 +22,7 @@ import "github.com/tdewolff/canvas" // 返回: bool 是否加载成功 func (r *Renderer) loadDefaultFonts() bool { for _, name := range fontDefaultSystemNames() { - if err := r.fontFamily.LoadSystemFont(name, canvas.FontRegular); err == nil { + if err := r.canvasState().fontFamily.LoadSystemFont(name, canvas.FontRegular); err == nil { return true } } diff --git a/ofdgo_renderer_font_loader_canvas.go b/ofdgo_renderer_font_loader_canvas.go index d018cbb..28dc7db 100644 --- a/ofdgo_renderer_font_loader_canvas.go +++ b/ofdgo_renderer_font_loader_canvas.go @@ -64,10 +64,10 @@ type fontCacheKey struct { // 入参: fontID 字体ID // 返回: *canvas.FontFamily 字体族 func (r *Renderer) loadFont(fontID string) *canvas.FontFamily { - if ff, ok := r.fontMap[fontID]; ok { + if ff, ok := r.canvasState().fontMap[fontID]; ok { return ff } - resolved, err := r.ResolveFont(fontID, false) + resolved, err := r.PrepareFont(fontID) if err != nil { r.renderError = err return nil @@ -82,34 +82,12 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily { fontData := resolved.Data fontStyle := canvasFontStyle(of) ff := canvas.NewFontFamily(of.FontName) - if of.FontFile != "" { - if cidMap := getCFFCIDRuneMap(fontData); len(cidMap) > 0 { - r.fontCIDMap[fontID] = cidMap - } - if _, fixedData, mapping, _, err := FixFontDataAggressive(fontData, true, true); err == nil { - fontData = fixedData - if mapping != nil { - inv := make(map[uint16]rune) - for k, v := range mapping { - if k == packedGlyphRune(v) { - inv[v] = k - } - } - for k, v := range mapping { - if _, ok := inv[v]; !ok { - inv[v] = k - } - } - r.fontGIDMap[fontID] = inv - } - } - } if err := ff.LoadFont(fontData, 0, fontStyle); err != nil { r.renderError = err return nil } if r.Reader.fontCache[fontID] != nil { - r.fontMap[fontID] = ff + r.canvasState().fontMap[fontID] = ff } return ff } @@ -132,7 +110,7 @@ func canvasFontStyle(font *Font) canvas.FontStyle { // 入参: fontID 字体ID, font OFD字体定义, style Canvas字体样式 // 返回: []fontSource 字体来源列表 func (r *Renderer) fontSources(fontID string, font *Font, style canvas.FontStyle) []fontSource { - if sources, ok := r.fontSourceCache[fontID]; ok { + if sources, ok := r.canvasState().fontSourceCache[fontID]; ok { return sources } bold := style&canvas.FontBold != 0 @@ -181,7 +159,7 @@ func (r *Renderer) fontSources(fontID string, font *Font, style canvas.FontStyle sources = appendFontSource(sources, seen, fontSource{kind: fontSourceFS, index: index, name: name}) } } - r.fontSourceCache[fontID] = sources + r.canvasState().fontSourceCache[fontID] = sources return sources } @@ -212,8 +190,8 @@ func appendFontSource(sources []fontSource, seen map[fontSourceKey]bool, source // 入参: fontID 字体ID, font OFD字体定义, exact 是否禁止无关回退 // 返回: fontSource 字体来源, *canvas.FontFamily 已加载的字体族,未匹配时为空 func (r *Renderer) fontSourceMatch(fontID string, font *Font, exact bool) (fontSource, *canvas.FontFamily) { - if source, ok := r.fontSourceUsed[fontID]; ok && (!exact || source.exact) && r.fontMap[fontID] != nil { - return source, r.fontMap[fontID] + if source, ok := r.canvasState().fontSourceUsed[fontID]; ok && (!exact || source.exact) && r.canvasState().fontMap[fontID] != nil { + return source, r.canvasState().fontMap[fontID] } style := canvasFontStyle(font) for _, source := range r.fontSources(fontID, font, style) { @@ -221,7 +199,7 @@ func (r *Renderer) fontSourceMatch(fontID string, font *Font, exact bool) (fontS continue } key := fontCacheKey{fontSourceKey: fontSourceKey{kind: source.kind, index: source.index, name: source.name, face: source.face}, style: style} - if cached, ok := r.fontCache[key]; ok { + if cached, ok := r.canvasState().fontCache[key]; ok { if cached != nil { return source, cached } @@ -240,7 +218,7 @@ func (r *Renderer) fontSourceMatch(fontID string, font *Font, exact bool) (fontS // 返回: *canvas.FontFamily 字体族 func (r *Renderer) loadFontSource(family *canvas.FontFamily, source fontSource, style canvas.FontStyle) *canvas.FontFamily { key := fontCacheKey{fontSourceKey: fontSourceKey{kind: source.kind, index: source.index, name: source.name, face: source.face}, style: style} - if cached, ok := r.fontCache[key]; ok { + if cached, ok := r.canvasState().fontCache[key]; ok { return cached } var err error @@ -262,10 +240,10 @@ func (r *Renderer) loadFontSource(family *canvas.FontFamily, source fontSource, } } if err != nil { - r.fontCache[key] = nil + r.canvasState().fontCache[key] = nil return nil } - r.fontCache[key] = family + r.canvasState().fontCache[key] = family return family } @@ -273,7 +251,7 @@ func (r *Renderer) loadFontSource(family *canvas.FontFamily, source fontSource, // 入参: dir 字体目录, names 字体名称, bold 是否粗体, italic 是否斜体 // 返回: []fontFileMatch 字体匹配列表 func (r *Renderer) matchFontFiles(dir string, names []string, bold, italic bool) []fontFileMatch { - candidates, ok := r.fontDirCandidates[dir] + candidates, ok := r.canvasState().fontDirCandidates[dir] if !ok { files, _ := filepath.Glob(filepath.Join(dir, "*")) candidates = fontFileCandidates(files, filepath.Base) @@ -285,7 +263,7 @@ func (r *Renderer) matchFontFiles(dir string, names []string, bold, italic bool) candidates = appendFontFileNames(candidates, i, fontFileNames(file)) file.Close() } - r.fontDirCandidates[dir] = candidates + r.canvasState().fontDirCandidates[dir] = candidates } return fontFileMatches(candidates, names, bold, italic) } @@ -298,7 +276,7 @@ func (r *Renderer) matchFontFS(index int, names []string, bold, italic bool) []f if fonts, ok := fsys.(*FontFS); ok { return fonts.matchStyle(names, bold, italic) } - candidates, ok := r.fontFSCandidates[index] + candidates, ok := r.canvasState().fontFSCandidates[index] if !ok { files, _ := fs.Glob(fsys, "*") candidates = fontFileCandidates(files, path.Base) @@ -315,7 +293,7 @@ func (r *Renderer) matchFontFS(index int, names []string, bold, italic bool) []f candidates = appendFontFileNames(candidates, i, fontFileNames(reader)) file.Close() } - r.fontFSCandidates[index] = candidates + r.canvasState().fontFSCandidates[index] = candidates } return fontFileMatches(candidates, names, bold, italic) } diff --git a/ofdgo_renderer_geom_canvas.go b/ofdgo_renderer_geom_canvas.go index 4095291..5e36327 100644 --- a/ofdgo_renderer_geom_canvas.go +++ b/ofdgo_renderer_geom_canvas.go @@ -21,6 +21,56 @@ import ( "github.com/tdewolff/canvas" ) +// canvasStrokeOptions 转换Canvas描边样式,虚线由调用方传入 +// 入参: width 描边宽度, cap 线帽, join 连接, tolerance 展开误差 +// 返回: StrokeOptions 公共描边样式 +func canvasStrokeOptions(width float64, cap canvas.Capper, join canvas.Joiner, tolerance float64) StrokeOptions { + options := StrokeOptions{Width: width, Cap: "Butt", Join: "Miter", MiterLimit: defaultMiterLimit, Tolerance: tolerance} + switch cap.(type) { + case canvas.RoundCapper: + options.Cap = "Round" + case canvas.SquareCapper: + options.Cap = "Square" + } + switch value := join.(type) { + case canvas.RoundJoiner: + options.Join = "Round" + case canvas.BevelJoiner: + options.Join = "Bevel" + case canvas.MiterJoiner: + options.MiterLimit = value.Limit + } + return options +} + +// canvasStrokePath 通过配置的几何后端生成描边,不修改源路径 +// 入参: geometry 几何后端, path Canvas路径, options 描边样式 +// 返回: *canvas.Path 描边轮廓, error 几何错误 +func canvasStrokePath(geometry GeometryBackend, path *canvas.Path, options StrokeOptions) (*canvas.Path, error) { + outline, err := geometry.Stroke(*geometryFromCanvasPath(path), options) + if err != nil { + return nil, err + } + return geometryToCanvasPath(&outline) +} + +// strokeCanvasPath 将文字和复杂画刷的描边交给配置的几何后端 +// 入参: path Canvas路径, width 描边宽度, cap 线帽, join 连接 +// 返回: *canvas.Path 描边轮廓 +func (r *Renderer) strokeCanvasPath(path *canvas.Path, width float64, cap canvas.Capper, join canvas.Joiner) *canvas.Path { + geometry, err := r.Geometry() + if err != nil { + r.renderError = err + return &canvas.Path{} + } + result, err := canvasStrokePath(geometry, path, canvasStrokeOptions(width, cap, join, canvas.Tolerance)) + if err != nil { + r.renderError = err + return &canvas.Path{} + } + return result +} + // Path 解析对象的标准紧缩路径并转换到页面坐标 // 入参: object 路径对象 // 返回: GeometryPath 页面路径, error 无效路径错误 diff --git a/ofdgo_renderer_glyph_canvas.go b/ofdgo_renderer_glyph_canvas.go index 80bd1e6..79985dc 100644 --- a/ofdgo_renderer_glyph_canvas.go +++ b/ofdgo_renderer_glyph_canvas.go @@ -92,11 +92,11 @@ func (r *Renderer) cachedTextGlyphPath(face *canvas.FontFace, glyph textGlyph) ( direction: face.Direction, glyph: glyph, } - if cached, ok := r.textGlyphPathCache[key]; ok { + if cached, ok := r.canvasState().textGlyphPathCache[key]; ok { return cached.path, cached.width } path, width := textGlyphPath(face, glyph) - r.textGlyphPathCache[key] = textGlyphPathCacheValue{path: path, width: width} + r.canvasState().textGlyphPathCache[key] = textGlyphPathCacheValue{path: path, width: width} return path, width } diff --git a/ofdgo_renderer_image_canvas.go b/ofdgo_renderer_image_canvas.go index 98b137f..81d165c 100644 --- a/ofdgo_renderer_image_canvas.go +++ b/ofdgo_renderer_image_canvas.go @@ -15,10 +15,9 @@ package ofdgo import ( - "bufio" + "bytes" "fmt" "image" - "io" "math" "github.com/tdewolff/canvas" @@ -90,12 +89,35 @@ func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float m[0][2] -= m[0][0]*p + m[0][1]*p m[1][2] -= m[1][0]*p + m[1][1]*p } - ctx.RenderImage(img, ctx.CoordSystemView().Mul(ctx.View()).Mul(m)) + ctx.RenderImage(r.canvasEncodedImage(img), ctx.CoordSystemView().Mul(ctx.View()).Mul(m)) if obj.Border != nil { r.renderImageBorder(ctx, obj, box, pageH, parentCTM, boundaryInCTM, clipPath) } } +// canvasEncodedImage 在Canvas输出边界保留原始编码,不重新压缩图片 +// 入参: img 库图片 +// 返回: image.Image Canvas可直接嵌入的图片 +func (r *Renderer) canvasEncodedImage(img image.Image) image.Image { + source, ok := img.(*EncodedImage) + if !ok { + return img + } + state := r.canvasState() + if encoded, ok := state.images[source]; ok { + return encoded + } + var encoded *canvasimage.Image + if source.format == "jpeg" { + encoded, _ = canvasimage.NewJPEGImage(bytes.NewReader(source.Bytes())) + } else { + encoded, _ = canvasimage.NewPNGImage(bytes.NewReader(source.Bytes())) + } + encoded.Bytes = source.Bytes() + state.images[source] = encoded + return encoded +} + // renderImageBorder 渲染图像边框 // 入参: ctx 画布上下文, obj 图片对象, box 图像边界, pageH 页面高度, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM, clipPath 裁剪路径 func (r *Renderer) renderImageBorder(ctx *canvas.Context, obj ImageObject, box Box, pageH float64, parentCTM *Matrix, boundaryInCTM bool, clipPath *canvas.Path) { @@ -166,7 +188,7 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva bounds, w, h = resized.Bounds(), nw, nh } clip := clipPath.Copy().Transform(m.Inv()) - compiler := &canvasPageCompiler{page: &RasterPage{Width: float64(w), Height: float64(h), DPI: 25.4}} + compiler := &canvasPageCompiler{page: &RasterPage{Width: float64(w), Height: float64(h), DPI: 25.4}, geometry: r.backends.Geometry} ctx := canvas.NewContext(compiler) ctx.SetFillColor(canvas.White) ctx.SetStrokeColor(canvas.Transparent) @@ -216,61 +238,3 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva } return out } - -// decodeImageResource 解码图片资源 -// 入参: resPath 图片资源路径 -// 返回: image.Image 图片对象, error 错误信息 -func (r *Renderer) decodeImageResource(resPath string) (image.Image, error) { - resPath = cleanPackagePath(r.Reader.ResPath(resPath)) - img, ok := r.imageCache[resPath] - if !ok { - var err error - img, err = r.readImageResource(resPath) - if err != nil { - return nil, err - } - if r.imageCache == nil { - r.imageCache = make(map[string]image.Image) - } - r.imageCache[resPath] = img - } - if r.decodeImages { - if source, ok := img.(*canvasimage.Image); ok { - return source.Image() - } - } - return img, nil -} - -// readImageResource 读取图片资源并保留可直接嵌入的原始编码 -// 入参: resPath 图片资源路径 -// 返回: image.Image 图片对象, error 错误信息 -func (r *Renderer) readImageResource(resPath string) (image.Image, error) { - rc, err := r.Reader.openFile(resPath) - if err != nil { - return nil, err - } - defer rc.Close() - reader := bufio.NewReaderSize(rc, 8) - header, _ := reader.Peek(8) - if isJPEGData(header) { - img, err := canvasimage.NewJPEGImage(reader) - if err != nil { - return nil, err - } - return img, nil - } - if isPNGData(header) { - img, err := canvasimage.NewPNGImage(reader) - if err != nil { - return nil, err - } - return img, nil - } - data, err := io.ReadAll(reader) - if err != nil { - return nil, err - } - img, _, err := decodeImageData(data) - return img, err -} diff --git a/ofdgo_renderer_layer_canvas.go b/ofdgo_renderer_layer_canvas.go index 8c5bece..19f6adb 100644 --- a/ofdgo_renderer_layer_canvas.go +++ b/ofdgo_renderer_layer_canvas.go @@ -16,159 +16,101 @@ package ofdgo import "github.com/tdewolff/canvas" -// renderAnnotations 渲染页面注释外观 -// 入参: ctx 画布上下文, pageID 页面ID, pageH 页面高度 -func (r *Renderer) renderAnnotations(ctx *canvas.Context, pageID string, pageH float64) { - for _, annot := range r.Reader.Annots[pageID] { - if annot.Visible != nil && !*annot.Visible { - continue - } - groups, grouped := ctx.Renderer.(interface { - beginAnnotation(string) bool - endObject() - }) - grouped = grouped && groups.beginAnnotation(annot.ID) - box, _ := ParseBox(annot.Appearance.Boundary) - ctm := Matrix{a: 1, d: 1, e: box.X, f: box.Y} - for i := range annot.Appearance.Objects { - r.renderObject(ctx, &annot.Appearance.Objects[i], pageH, nil, &ctm, false, nil) - } - if grouped { - groups.endObject() - } - } +// canvasPageVisitor 将公共页面遍历结果交给Canvas解释器 +type canvasPageVisitor struct { + renderer *Renderer + context *canvas.Context + height float64 } -// renderTemplate 渲染模板 -// 入参: ctx 画布上下文, templateID 模板ID, pageH 页面高度 -func (r *Renderer) renderTemplate(ctx *canvas.Context, templateID string, pageH float64) { - tplContent := r.loadTemplate(templateID) - if tplContent == nil { - return +// DrawObject 绘制叶子对象,转换页面坐标裁剪 +// 入参: object 源对象, state 继承状态 +// 返回: error 绘制错误 +func (v *canvasPageVisitor) DrawObject(object *GraphicObject, state RenderState) error { + clip, err := geometryToCanvasPath(state.Clip) + if err != nil { + return err } - for order := range 3 { - r.renderLayers(ctx, tplContent.Content.Layer, pageH, order) + if clip != nil { + clip = clip.Translate(0, v.height) } + switch object.Type { + case "TextObject": + v.renderer.renderText(v.context, object.TextObject, v.height, state.Defaults, state.Parent, state.BoundaryInCTM, clip) + case "PathObject": + v.renderer.renderPath(v.context, object.PathObject, v.height, state.Defaults, state.Parent, state.BoundaryInCTM, clip) + case "ImageObject": + v.renderer.renderImage(v.context, object.ImageObject, v.height, state.Parent, state.BoundaryInCTM, clip) + } + return v.renderer.renderError } -// renderLayers 按原顺序渲染指定类型的图层 -// 入参: ctx 画布上下文, layers 图层列表, pageH 页面高度, order 图层类型顺序 -func (r *Renderer) renderLayers(ctx *canvas.Context, layers []Layer, pageH float64, order int) { - for _, layer := range layers { - if layerOrder(layer.Type) == order { - r.renderLayer(ctx, layer, pageH, nil, nil) - } - } +// DrawStamp 绘制签章外观 +// 入参: stamp 签章 +// 返回: error 绘制错误 +func (v *canvasPageVisitor) DrawStamp(stamp Stamp) error { + v.renderer.renderStamp(v.context, stamp, v.height) + return v.renderer.renderError } -// renderLayer 渲染图层 -// 入参: ctx 画布上下文, layer 图层对象, pageH 页面高度, defaults 默认绘制参数, parentCTM 父级CTM -func (r *Renderer) renderLayer(ctx *canvas.Context, layer Layer, pageH float64, defaults *DrawParam, parentCTM *Matrix) { - defaults = r.drawParamDefaults(layer.DrawParam, defaults) - if len(layer.Objects) > 0 { - for i := range layer.Objects { - r.renderObject(ctx, &layer.Objects[i], pageH, defaults, parentCTM, false, nil) - } - return - } - for _, textObj := range layer.TextObject { - r.renderText(ctx, textObj, pageH, defaults, parentCTM, false, nil) - } - for _, pathObj := range layer.PathObject { - r.renderPath(ctx, pathObj, pageH, defaults, parentCTM, false, nil) - } - for _, imgObj := range layer.ImageObject { - r.renderImage(ctx, imgObj, pageH, parentCTM, false, nil) - } - for _, cgu := range layer.CompositeGraphicUnit { - r.renderCompositeGraphicUnit(ctx, cgu, pageH, defaults, parentCTM, false, nil) - } -} - -// renderCompositeGraphicUnit 渲染复合图元 -// 入参: ctx 画布上下文, cgu 复合图元对象, pageH 页面高度, defaults 默认绘制参数, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换, parentClip 父级裁剪路径 -func (r *Renderer) renderCompositeGraphicUnit(ctx *canvas.Context, cgu CompositeGraphicUnit, pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) { - if cgu.Visible != nil && !*cgu.Visible { - if groups, ok := ctx.Renderer.(interface{ skipObjects(int) }); ok { - count, ref, seen := len(cgu.Objects), cgu.ResourceID, make(map[string]bool) - for ref != "" && !seen[ref] { - seen[ref] = true - unit := r.CompositeGraphicUnits[ref] - if unit == nil { - break - } - count, ref = count+len(unit.Objects), unit.ResourceID - } - groups.skipObjects(count) - } - return - } - ctx.Push() - box, _ := ParseBox(cgu.Boundary) - boundaryCTM := TranslationMatrix(box.X, box.Y) - if parentCTM != nil { - boundaryCTM = parentCTM.Multiply(boundaryCTM) - } - currentCTM := boundaryCTM.Multiply(NewMatrix(cgu.CTM)) - clips, clipCTM := cgu.Clips, currentCTM - if clips != nil && clips.TransFlag != nil && !*clips.TransFlag { - clipCopy := *clips - clipCopy.TransFlag = nil - clips, clipCTM = &clipCopy, boundaryCTM - } - clipPath := intersectClipPath(parentClip, r.buildClipPath(clips, pageH, 0, 0, clipCTM)) - defaults = r.drawParamDefaults(cgu.DrawParam, defaults) - if cgu.ResourceID != "" { - if ref, ok := r.CompositeGraphicUnits[cgu.ResourceID]; ok { - refCopy := *ref - refCopy.Alpha = mergeAlpha(refCopy.Alpha, cgu.Alpha) - r.renderCompositeGraphicUnit(ctx, refCopy, pageH, defaults, ¤tCTM, true, clipPath) - } - } - if len(cgu.Objects) > 0 { - for _, obj := range cgu.Objects { - obj = mergeGraphicObjectAlpha(obj, cgu.Alpha) - r.renderObject(ctx, &obj, pageH, defaults, ¤tCTM, boundaryInCTM, clipPath) - } - ctx.Pop() - return - } - for _, imgObj := range cgu.ImageObject { - imgObj.Alpha = mergeAlpha(imgObj.Alpha, cgu.Alpha) - r.renderImage(ctx, imgObj, pageH, ¤tCTM, boundaryInCTM, clipPath) - } - for _, pathObj := range cgu.PathObject { - pathObj.Alpha = mergeAlpha(pathObj.Alpha, cgu.Alpha) - r.renderPath(ctx, pathObj, pageH, defaults, ¤tCTM, boundaryInCTM, clipPath) - } - for _, textObj := range cgu.TextObject { - textObj.Alpha = mergeAlpha(textObj.Alpha, cgu.Alpha) - r.renderText(ctx, textObj, pageH, defaults, ¤tCTM, boundaryInCTM, clipPath) - } - for _, subCgu := range cgu.CompositeGraphicUnit { - subCgu.Alpha = mergeAlpha(subCgu.Alpha, cgu.Alpha) - r.renderCompositeGraphicUnit(ctx, subCgu, pageH, defaults, ¤tCTM, boundaryInCTM, clipPath) - } - ctx.Pop() -} - -// renderObject 渲染图形对象 -// 入参: ctx 画布上下文, obj 图形对象, pageH 页面高度, defaults 默认绘制参数, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换, parentClip 父级裁剪路径 -func (r *Renderer) renderObject(ctx *canvas.Context, obj *GraphicObject, pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) { - if groups, ok := ctx.Renderer.(interface { +// BeginObject 开始可编辑对象分组 +// 入参: object 源对象 +// 返回: bool 是否建立分组 +func (v *canvasPageVisitor) BeginObject(object *GraphicObject) bool { + groups, ok := v.context.Renderer.(interface { beginObject(*GraphicObject) bool endObject() - }); ok && groups.beginObject(obj) { - defer groups.endObject() + }) + return ok && groups.beginObject(object) +} + +// BeginAnnotation 开始注解分组 +// 入参: id 注解标识 +// 返回: bool 是否建立分组 +func (v *canvasPageVisitor) BeginAnnotation(id string) bool { + groups, ok := v.context.Renderer.(interface { + beginAnnotation(string) bool + endObject() + }) + return ok && groups.beginAnnotation(id) +} + +// EndObject 结束对象或注解分组 +func (v *canvasPageVisitor) EndObject() { + v.context.Renderer.(interface{ endObject() }).endObject() +} + +// SkipObjects 保留不可见对象的编号 +// 入参: count 对象数量 +func (v *canvasPageVisitor) SkipObjects(count int) { + if groups, ok := v.context.Renderer.(interface{ skipObjects(int) }); ok { + groups.skipObjects(count) } - switch obj.Type { - case "TextObject": - r.renderText(ctx, obj.TextObject, pageH, defaults, parentCTM, boundaryInCTM, parentClip) - case "PathObject": - r.renderPath(ctx, obj.PathObject, pageH, defaults, parentCTM, boundaryInCTM, parentClip) - case "ImageObject": - r.renderImage(ctx, obj.ImageObject, pageH, parentCTM, boundaryInCTM, parentClip) - case "CompositeGraphicUnit", "CompositeObject": - r.renderCompositeGraphicUnit(ctx, obj.CompositeGraphicUnit, pageH, defaults, parentCTM, boundaryInCTM, parentClip) +} + +// canvasRenderState 将Canvas裁剪转换为公共页面状态 +// 入参: pageH 页面高度, defaults 默认样式, parentCTM 父变换, boundaryInCTM 边界是否参与变换, parentClip 父裁剪 +// 返回: RenderState 公共页面状态 +func canvasRenderState(pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) RenderState { + var clip *GeometryPath + if parentClip != nil { + clip = geometryFromCanvasPath(parentClip.Copy().Translate(0, -pageH)) + } + return RenderState{Defaults: defaults, Parent: parentCTM, BoundaryInCTM: boundaryInCTM, Clip: clip} +} + +// renderCompositeGraphicUnit 通过公共遍历绘制复合图元 +// 入参: ctx 画布上下文, cgu 复合图元, pageH 页面高度, defaults 默认样式, parentCTM 父变换, boundaryInCTM 边界是否参与变换, parentClip 父裁剪 +func (r *Renderer) renderCompositeGraphicUnit(ctx *canvas.Context, cgu CompositeGraphicUnit, pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) { + if err := r.walkComposite(cgu, canvasRenderState(pageH, defaults, parentCTM, boundaryInCTM, parentClip), &canvasPageVisitor{r, ctx, pageH}, nil); err != nil { + r.renderError = err + } +} + +// renderObject 通过公共遍历绘制图形对象 +// 入参: ctx 画布上下文, obj 图形对象, pageH 页面高度, defaults 默认样式, parentCTM 父变换, boundaryInCTM 边界是否参与变换, parentClip 父裁剪 +func (r *Renderer) renderObject(ctx *canvas.Context, obj *GraphicObject, pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) { + if err := r.WalkObject(obj, canvasRenderState(pageH, defaults, parentCTM, boundaryInCTM, parentClip), &canvasPageVisitor{r, ctx, pageH}); err != nil { + r.renderError = err } } diff --git a/ofdgo_renderer_page_canvas.go b/ofdgo_renderer_page_canvas.go index 06ba0fc..2bd21cb 100644 --- a/ofdgo_renderer_page_canvas.go +++ b/ofdgo_renderer_page_canvas.go @@ -63,27 +63,8 @@ func (r *Renderer) renderCanvasPageToContext(ctx *canvas.Context, page *PageCont ctx.SetFillColor(canvas.White) ctx.DrawPath(0, 0, canvas.Rectangle(box.W, box.H)) } - for order := range 3 { - if r.Reader.doc != nil { - for _, tplRef := range page.Template { - kind := tplRef.ZOrder - if kind == "" { - kind = "Background" - } - if layerOrder(kind) == order { - r.renderTemplate(ctx, tplRef.TemplateID, pageH) - } - } - } - r.renderLayers(ctx, page.Content.Layer, pageH, order) - } - if r.RenderAnnotations { - r.renderAnnotations(ctx, page.ID, pageH) - } - if stamps, ok := r.Reader.Stamps[page.ID]; ok { - for _, stamp := range stamps { - r.renderStamp(ctx, stamp, pageH) - } + if err := r.WalkPage(page, &canvasPageVisitor{r, ctx, pageH}); err != nil { + return err } if r.renderError != nil { return r.renderError diff --git a/ofdgo_renderer_paint_canvas.go b/ofdgo_renderer_paint_canvas.go index 76fa3cb..2ede48e 100644 --- a/ofdgo_renderer_paint_canvas.go +++ b/ofdgo_renderer_paint_canvas.go @@ -15,27 +15,24 @@ package ofdgo import ( - "github.com/tdewolff/canvas" "math" + + "github.com/tdewolff/canvas" ) -// parseFillPaint 解析填充画刷 +// parseFillPaint 将公共画刷转换为Canvas对象 // 入参: fillColor 填充颜色节点, x X坐标, y Y坐标, pageH 页面高度 // 返回: any 填充画刷 func (r *Renderer) parseFillPaint(fillColor *FillColor, x, y, pageH float64) any { - if fillColor == nil { + paint := r.ResolvePaint(fillColor) + switch paint.Kind { + case PaintNone, PaintPattern: return nil + case PaintLinear, PaintRadial: + return canvasShading(paint, x, y, pageH) + default: + return paint.Color } - if fillColor.Pattern != nil { - return nil - } - if gradient := r.parseAxialShdGradient(fillColor.AxialShd, fillColor.Alpha, x, y, pageH); gradient != nil { - return newShdPaint(gradient, fillColor.AxialShd.Extend, fillColor.AxialShd.MapType, fillColor.AxialShd.MapUnit) - } - if paint := r.parseRadialShdPaint(fillColor.RadialShd, fillColor.Alpha, x, y, pageH); paint != nil { - return paint - } - return r.parseFillColor(fillColor) } // parseStrokePaint 解析勾边画刷 @@ -45,39 +42,34 @@ func (r *Renderer) parseStrokePaint(strokeColor *StrokeColor, x, y, pageH float6 return r.parseFillPaint((*FillColor)(strokeColor), x, y, pageH) } -// parseShdSegments 将通用渐变分段转换为Canvas画刷 -// 入参: segments 渐变分段, alpha 透明度 +// canvasGradient 将公共颜色节点转换为Canvas渐变 +// 入参: stops 渐变分段 // 返回: canvas.Grad 渐变分段 -func (r *Renderer) parseShdSegments(segments []ShdSegment, alpha *int) canvas.Grad { +func canvasGradient(stops []ColorStop) canvas.Grad { var gradient canvas.Grad - for _, stop := range r.GradientStops(segments, alpha) { + for _, stop := range stops { gradient.Add(stop.Offset, stop.Color) } return gradient } -// parseAxialShdGradient 解析轴向渐变 -// 入参: axialShd 轴向渐变节点, alpha 透明度, x X坐标, y Y坐标, pageH 页面高度 -// 返回: canvas.Gradient 渐变对象 -func (r *Renderer) parseAxialShdGradient(axialShd *AxialShd, alpha *int, x, y, pageH float64) canvas.Gradient { - if axialShd == nil { - return nil +// canvasShading 转换坐标与椭圆渐变变换,不重新解析OFD样式 +// 入参: paint 公共渐变画刷, x X坐标, y Y坐标, pageH 页面高度 +// 返回: *shdPaint Canvas渐变画刷 +func canvasShading(paint Paint, x, y, pageH float64) *shdPaint { + source := paint.Gradient + gradient := canvasGradient(source.Stops) + start := canvas.Point{X: x + source.Start.X, Y: pageH - y - source.Start.Y} + end := canvas.Point{X: x + source.End.X, Y: pageH - y - source.End.Y} + if paint.Kind == PaintLinear { + return newShdPaint(gradient.ToLinear(start, end), source.Extend, source.MapType, source.MapUnit) } - start := parseFloats(axialShd.StartPoint) - end := parseFloats(axialShd.EndPoint) - if len(start) < 2 || len(end) < 2 { - return nil + result := newShdPaint(gradient.ToRadial(start, source.StartRadius, end, source.EndRadius), source.Extend, source.MapType, source.MapUnit) + if e := source.Eccentricity; 0 < e && e < 1 { + result.view = canvas.Identity.Translate(start.X, start.Y).Rotate(-source.Angle).Scale(1, math.Sqrt(1-e*e)).Translate(-start.X, -start.Y) + result.gradient = gradient.ToRadial(start, source.StartRadius, result.view.Inv().Dot(end), source.EndRadius) } - gradient := r.parseShdSegments(axialShd.Segment, alpha) - if gradient == nil { - return nil - } - startPoint := canvas.Point{X: x + start[0], Y: pageH - (y + start[1])} - endPoint := canvas.Point{X: x + end[0], Y: pageH - (y + end[1])} - if startPoint.Equals(endPoint) { - return nil - } - return gradient.ToLinear(startPoint, endPoint) + return result } // axialShdClip 获取轴向渐变的延伸裁剪区域 @@ -101,29 +93,3 @@ func axialShdClip(gradient *canvas.LinearGradient, extend int, bounds canvas.Rec } return area.ToPath().Transform(axis) } - -// parseRadialShdPaint 解析径向渐变画刷 -// 入参: radialShd 径向渐变节点, alpha 透明度, x X坐标, y Y坐标, pageH 页面高度 -// 返回: *shdPaint 渐变画刷 -func (r *Renderer) parseRadialShdPaint(radialShd *RadialShd, alpha *int, x, y, pageH float64) *shdPaint { - if radialShd == nil || radialShd.EndRadius <= 0 { - return nil - } - start := parseFloats(radialShd.StartPoint) - end := parseFloats(radialShd.EndPoint) - if len(start) < 2 || len(end) < 2 { - return nil - } - gradient := r.parseShdSegments(radialShd.Segment, alpha) - if gradient == nil { - return nil - } - startPoint := canvas.Point{X: x + start[0], Y: pageH - (y + start[1])} - endPoint := canvas.Point{X: x + end[0], Y: pageH - (y + end[1])} - paint := newShdPaint(gradient.ToRadial(startPoint, radialShd.StartRadius, endPoint, radialShd.EndRadius), radialShd.Extend, radialShd.MapType, radialShd.MapUnit) - if e := radialShd.Eccentricity; 0 < e && e < 1 { - paint.view = canvas.Identity.Translate(startPoint.X, startPoint.Y).Rotate(-radialShd.Angle).Scale(1, math.Sqrt(1-e*e)).Translate(-startPoint.X, -startPoint.Y) - paint.gradient = gradient.ToRadial(startPoint, radialShd.StartRadius, paint.view.Inv().Dot(endPoint), radialShd.EndRadius) - } - return paint -} diff --git a/ofdgo_renderer_path_canvas.go b/ofdgo_renderer_path_canvas.go index 87e79ba..ab0aad9 100644 --- a/ofdgo_renderer_path_canvas.go +++ b/ofdgo_renderer_path_canvas.go @@ -29,8 +29,8 @@ type pathStyle struct { strokeColor color.Color fillPaint any strokePaint any - fillPattern *patternPaint - strokePattern *patternPaint + fillPattern *PatternPaint + strokePattern *PatternPaint lineWidth float64 lineCap canvas.Capper lineJoin canvas.Joiner @@ -280,9 +280,10 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64 } strokeClip := intersectClipPath(clipPath, shadingClip) _, repeat := strokePaint.(*repeatAxialGradient) - if strokeClip != nil || strokeView != canvas.Identity || repeat || style.strokePattern != nil { + _, nativeGeometry := r.backends.Geometry.(CanvasBackend) + if strokeClip != nil || strokeView != canvas.Identity || repeat || style.strokePattern != nil || !nativeGeometry { sp = sp.Copy() - sp = sp.Stroke(style.lineWidth, style.lineCap, style.lineJoin, canvas.Tolerance) + sp = r.strokeCanvasPath(sp, style.lineWidth, style.lineCap, style.lineJoin) sp = applyClipPath(sp, strokeClip) if style.strokePattern != nil { r.renderPattern(ctx, style.strokePattern, pageH, sp, objectCTM) @@ -300,9 +301,9 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64 // renderPattern 渲染图案填充 // 入参: ctx 画布上下文, pattern 底纹画刷, pageH 页面高度, clip 绘制区域, objectCTM 对象变换矩阵 -func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *patternPaint, pageH float64, clip *canvas.Path, objectCTM Matrix) { +func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *PatternPaint, pageH float64, clip *canvas.Path, objectCTM Matrix) { if bounds, ok := ctx.Renderer.(*boundsRenderer); ok { - if pattern.alpha == nil || *pattern.alpha > 0 { + if pattern.Alpha == nil || *pattern.Alpha > 0 { bounds.add(clip) } return @@ -358,7 +359,7 @@ func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *patternPaint, pag endX := int(math.Ceil(maxX/xStep)) + 1 startY := int(math.Floor(minY/yStep)) - 1 endY := int(math.Ceil(maxY/yStep)) + 1 - defaults := &DrawParam{FillColor: &pattern.color, StrokeColor: (*StrokeColor)(&pattern.color)} + defaults := &DrawParam{FillColor: &pattern.Color, StrokeColor: (*StrokeColor)(&pattern.Color)} for ix := startX; ix <= endX; ix++ { for iy := startY; iy <= endY; iy++ { tileCTM := patternCTM.Multiply(TranslationMatrix(float64(ix)*xStep, float64(iy)*yStep)) @@ -369,7 +370,7 @@ func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *patternPaint, pag tileCTM = tileCTM.Multiply(Matrix{a: 1, d: -1, f: pattern.Height}) } for _, obj := range pattern.CellContent.Objects { - obj = mergeGraphicObjectAlpha(obj, pattern.alpha) + obj = mergeGraphicObjectAlpha(obj, pattern.Alpha) r.renderObject(ctx, &obj, pageH, defaults, &tileCTM, true, clip) } } diff --git a/ofdgo_renderer_stamp_canvas.go b/ofdgo_renderer_stamp_canvas.go index 9fcd3f0..b40cbb1 100644 --- a/ofdgo_renderer_stamp_canvas.go +++ b/ofdgo_renderer_stamp_canvas.go @@ -120,6 +120,6 @@ func (r *Renderer) renderStampImage(ctx *canvas.Context, img image.Image, s Stam ctx.Push() ctx.Translate(box.X, pageH-(box.Y+box.H)) ctx.Scale(box.W/float64(img.Bounds().Dx()), box.H/float64(img.Bounds().Dy())) - ctx.DrawImage(0, 0, img, canvas.DPMM(1.0)) + ctx.DrawImage(0, 0, r.canvasEncodedImage(img), canvas.DPMM(1.0)) ctx.Pop() } diff --git a/ofdgo_renderer_state_canvas.go b/ofdgo_renderer_state_canvas.go new file mode 100644 index 0000000..2eccb98 --- /dev/null +++ b/ofdgo_renderer_state_canvas.go @@ -0,0 +1,58 @@ +// 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 ( + "image" + + "github.com/tdewolff/canvas" +) + +// canvasBackendState 保存Canvas字体、字形和编码图片适配缓存 +type canvasBackendState struct { + images map[*EncodedImage]image.Image + fontFamily *canvas.FontFamily + defaultFontLoaded bool + fontMap map[string]*canvas.FontFamily + fontCache map[fontCacheKey]*canvas.FontFamily + svgFontCache map[*canvas.Font]SVGFont + fontSourceCache map[string][]fontSource + fontSourceUsed map[string]fontSource + fontDirCandidates map[string][]fontFileCandidate + fontFSCandidates map[int][]fontFileCandidate + textGlyphPathCache map[textGlyphPathCacheKey]textGlyphPathCacheValue +} + +// canvasState 获取当前渲染器的Canvas私有缓存,按需创建且不向其他后端暴露 +// 返回: *canvasBackendState Canvas缓存 +func (r *Renderer) canvasState() *canvasBackendState { + key := CanvasBackend{} + if state, ok := r.backendStates[key]; ok { + return state.(*canvasBackendState) + } + state := &canvasBackendState{ + images: make(map[*EncodedImage]image.Image), + fontMap: make(map[string]*canvas.FontFamily), + fontCache: make(map[fontCacheKey]*canvas.FontFamily), + svgFontCache: make(map[*canvas.Font]SVGFont), + fontSourceCache: make(map[string][]fontSource), + fontSourceUsed: make(map[string]fontSource), + fontDirCandidates: make(map[string][]fontFileCandidate), + fontFSCandidates: make(map[int][]fontFileCandidate), + textGlyphPathCache: make(map[textGlyphPathCacheKey]textGlyphPathCacheValue), + } + r.backendStates[key] = state + return state +} diff --git a/ofdgo_renderer_svg_canvas.go b/ofdgo_renderer_svg_canvas.go index e04dab2..fcc20fc 100644 --- a/ofdgo_renderer_svg_canvas.go +++ b/ofdgo_renderer_svg_canvas.go @@ -186,7 +186,7 @@ func (s *svgResourceRenderer) RenderText(text *canvas.Text, m canvas.Matrix) { return } font := text.MostCommonFontFace().Font - resource, ok := s.renderer.svgFontCache[font] + resource, ok := s.renderer.canvasState().svgFontCache[font] if !ok { data := fontSFNTData(font.SFNT) resource = SVGFont{ @@ -198,7 +198,7 @@ func (s *svgResourceRenderer) RenderText(text *canvas.Text, m canvas.Matrix) { if font.Style().Italic() { resource.Style = "italic" } - s.renderer.svgFontCache[font] = resource + s.renderer.canvasState().svgFontCache[font] = resource } if !s.seen[resource.Name] { s.seen[resource.Name] = true diff --git a/ofdgo_renderer_text_canvas.go b/ofdgo_renderer_text_canvas.go index f8e2ba0..6acf567 100644 --- a/ofdgo_renderer_text_canvas.go +++ b/ofdgo_renderer_text_canvas.go @@ -76,7 +76,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 sizePt := sizeMM * ptPerMM fillColor := colorWithAlpha(canvas.Black, obj.Alpha) var fillPaint any = fillColor - var fillPattern *patternPaint + var fillPattern *PatternPaint var fillColorNode *FillColor if defaults != nil { fillColorNode = defaults.FillColor @@ -187,15 +187,6 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 hasUnderline := strings.Contains(obj.Decoration, "Underline") _, shadedFill := fillPaint.(canvas.Gradient) verticalAdvance := (obj.ReadDirection-obj.CharDirection)%180 != 0 - advanceX, advanceY := 1.0, 0.0 - switch obj.ReadDirection { - case 90: - advanceX, advanceY = 0, 1 - case 180: - advanceX = -1 - case 270: - advanceX, advanceY = 0, -1 - } codePos := 0 textPos := 0 for index, tc := range obj.TextCode { @@ -215,17 +206,9 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 if textRun != nil && tc.Index == "" { spans = textGlyphSpans(runes, glyphTransforms, codePos) } - dxs, dys := parseFloats(tc.DeltaX), parseFloats(tc.DeltaY) - xs, ys := parseFloats(tc.X), parseFloats(tc.Y) + positioner := NewTextPositioner(tc, obj.ReadDirection) + dxs, dys, xs, ys := positioner.dxs, positioner.dys, positioner.xs, positioner.ys drawAsPath := embeddedFont || face.FauxBold > 0 || textCodePositioned(tc, xs, ys) || fillClip != nil || shadedFill || fillPattern != nil || shouldStroke - cx, cy := 0.0, 0.0 - previousAdvance := 0.0 - if len(xs) > 0 { - cx = xs[0] - } - if len(ys) > 0 { - cy = ys[0] - } for i, glyph := range glyphs { str := glyph.Text drawAsGlyphPath := drawAsPath || glyph.GlyphID >= 0 @@ -244,28 +227,12 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 } else { glyphWidth = textGlyphWidth(face, glyph) } - if i < len(xs) { - cx = xs[i] - } else if i > 0 { - if dx, ok := textDelta(dxs, i-1); ok { - cx += dx - } else if len(dys) == 0 { - cx += previousAdvance * advanceX - } - } - if i < len(ys) { - cy = ys[i] - } else if i > 0 { - if dy, ok := textDelta(dys, i-1); ok { - cy += dy - } else if len(dxs) == 0 { - cy += previousAdvance * advanceY - } - } - previousAdvance = glyphWidth * hScale + advance := glyphWidth * hScale if verticalAdvance { - previousAdvance = sizeMM + advance = sizeMM } + position := positioner.Next(advance) + cx, cy := position.X, position.Y var canvasX, canvasY float64 if boundaryInCTM && parentCTM != nil { tx, ty := localCTM.Transform(cx, cy) @@ -328,7 +295,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 if len(strokeStyle.dashPattern) > 0 { path = path.Dash(strokeStyle.dashOffset, strokeStyle.dashPattern...) } - path = path.Stroke(strokeStyle.lineWidth, strokeStyle.lineCap, strokeStyle.lineJoin, canvas.Tolerance) + path = r.strokeCanvasPath(path, strokeStyle.lineWidth, strokeStyle.lineCap, strokeStyle.lineJoin) path = applyClipPath(path, strokeClip) if strokeStyle.strokePattern != nil { r.renderPattern(ctx, strokeStyle.strokePattern, pageH, path, objectCTM) @@ -341,7 +308,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 underline := &canvas.Path{} underline.MoveTo(0, -sizeMM*0.1) underline.LineTo(glyphWidth*scaleX, -sizeMM*0.1) - underline = underline.Stroke(sizeMM*0.05, canvas.ButtCap, canvas.MiterJoin, canvas.Tolerance) + underline = r.strokeCanvasPath(underline, sizeMM*0.05, canvas.ButtCap, canvas.MiterJoin) if shouldFill && fillPattern != nil { r.renderPattern(ctx, fillPattern, pageH, applyClipPath(underline.Transform(transform), clipPath), objectCTM) } else if shouldFill { @@ -380,7 +347,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 underline := &canvas.Path{} underline.MoveTo(0, -off) underline.LineTo(textWidth, -off) - underline = underline.Stroke(uw, canvas.ButtCap, canvas.MiterJoin, canvas.Tolerance) + underline = r.strokeCanvasPath(underline, uw, canvas.ButtCap, canvas.MiterJoin) underline = applyClipPath(underline.Transform(textTransform), clipPath) if fillPattern != nil { r.renderPattern(ctx, fillPattern, pageH, underline, objectCTM) diff --git a/ofdgo_text.go b/ofdgo_text.go index 22a1246..037df58 100644 --- a/ofdgo_text.go +++ b/ofdgo_text.go @@ -185,10 +185,14 @@ func (r *Renderer) fontGlyphRune(fontID string, glyphID int) (rune, bool) { return 0, false } id := uint16(glyphID) - if mapped, ok := r.fontCIDMap[fontID][id]; ok { + prepared := r.preparedFonts[fontID] + if prepared == nil { + return 0, false + } + if mapped, ok := prepared.CIDs[id]; ok { return mapped, true } - if mapped, ok := r.fontGIDMap[fontID][id]; ok { + if mapped, ok := prepared.Glyphs[id]; ok { return mapped, true } return 0, false diff --git a/ofdgo_text_position.go b/ofdgo_text_position.go new file mode 100644 index 0000000..821d013 --- /dev/null +++ b/ofdgo_text_position.go @@ -0,0 +1,68 @@ +// 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 + +// TextPositioner 按OFD坐标及增量定位连续字形,不依赖字体或绘图库 +type TextPositioner struct { + xs, ys, dxs, dys []float64 + direction Point + current Point + previous float64 + index int +} + +// NewTextPositioner 创建单个TextCode的定位器,方向采用OFD角度 +// 入参: code 文本定位数据, direction 阅读方向 +// 返回: 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 +} + +// Next 返回下一个字形位置并保存其推进长度,显式坐标和增量优先 +// 入参: advance 当前字形推进长度,单位为毫米 +// 返回: Point 字形原点 +func (p *TextPositioner) Next(advance float64) 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) { + p.current.Y = p.ys[i] + } 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 +}