mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-08-30 12:12:40 +08:00
This commit is contained in:
-1046
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,247 @@
|
||||
// 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"
|
||||
|
||||
"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 len(annot.Appearance.Objects) == 0 {
|
||||
continue
|
||||
}
|
||||
box, _ := ParseBox(annot.Appearance.Boundary)
|
||||
ctm := Matrix{a: 1, d: 1, e: box.X, f: box.Y}
|
||||
for _, obj := range annot.Appearance.Objects {
|
||||
r.renderObject(ctx, obj, pageH, nil, nil, 0, &ctm, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// renderTemplate 渲染模板
|
||||
// 入参: ctx 画布上下文, templateID 模板ID, pageH 页面高度
|
||||
func (r *Renderer) renderTemplate(ctx *canvas.Context, templateID string, pageH float64) {
|
||||
var tplPage *TemplatePage
|
||||
for _, tp := range r.Reader.doc.CommonData.TemplatePage {
|
||||
if tp.ID == templateID {
|
||||
tplPage = &tp
|
||||
break
|
||||
}
|
||||
}
|
||||
if tplPage == nil {
|
||||
return
|
||||
}
|
||||
tplContent, err := r.Reader.PageContent(Page{BaseLoc: tplPage.BaseLoc})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if tplContent.Content.Layer != nil {
|
||||
for _, layer := range tplContent.Content.Layer {
|
||||
r.renderLayer(ctx, layer, pageH, nil, nil, 0, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// renderLayer 渲染图层
|
||||
// 入参: ctx 画布上下文, layer 图层对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM
|
||||
func (r *Renderer) renderLayer(ctx *canvas.Context, layer Layer, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix) {
|
||||
defaultFill, defaultStroke, defaultLW = r.drawParamDefaults(layer.DrawParam, defaultFill, defaultStroke, defaultLW)
|
||||
if len(layer.Objects) > 0 {
|
||||
for _, obj := range layer.Objects {
|
||||
r.renderObject(ctx, obj, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, false)
|
||||
}
|
||||
return
|
||||
}
|
||||
for _, textObj := range layer.TextObject {
|
||||
r.renderText(ctx, textObj, pageH, defaultFill, defaultStroke, parentCTM, false)
|
||||
}
|
||||
for _, pathObj := range layer.PathObject {
|
||||
r.renderPath(ctx, pathObj, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, false)
|
||||
}
|
||||
for _, imgObj := range layer.ImageObject {
|
||||
r.renderImage(ctx, imgObj, pageH, parentCTM, false)
|
||||
}
|
||||
for _, cgu := range layer.CompositeGraphicUnit {
|
||||
r.renderCompositeGraphicUnit(ctx, cgu, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, false)
|
||||
}
|
||||
}
|
||||
|
||||
// renderCompositeGraphicUnit 渲染复合图元
|
||||
// 入参: ctx 画布上下文, cgu 复合图元对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换
|
||||
func (r *Renderer) renderCompositeGraphicUnit(ctx *canvas.Context, cgu CompositeGraphicUnit, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix, boundaryInCTM bool) {
|
||||
if cgu.Visible != nil && !*cgu.Visible {
|
||||
return
|
||||
}
|
||||
ctx.Push()
|
||||
currentCTM := NewMatrix(cgu.CTM)
|
||||
if parentCTM != nil {
|
||||
currentCTM = parentCTM.Multiply(currentCTM)
|
||||
}
|
||||
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, defaultFill, defaultStroke, defaultLW, ¤tCTM, true)
|
||||
}
|
||||
}
|
||||
defaultFill, defaultStroke, defaultLW = r.drawParamDefaults(cgu.DrawParam, defaultFill, defaultStroke, defaultLW)
|
||||
if len(cgu.Objects) > 0 {
|
||||
for _, obj := range cgu.Objects {
|
||||
obj = mergeGraphicObjectAlpha(obj, cgu.Alpha)
|
||||
r.renderObject(ctx, obj, pageH, defaultFill, defaultStroke, defaultLW, ¤tCTM, boundaryInCTM)
|
||||
}
|
||||
ctx.Pop()
|
||||
return
|
||||
}
|
||||
for _, imgObj := range cgu.ImageObject {
|
||||
imgObj.Alpha = mergeAlpha(imgObj.Alpha, cgu.Alpha)
|
||||
r.renderImage(ctx, imgObj, pageH, ¤tCTM, boundaryInCTM)
|
||||
}
|
||||
for _, pathObj := range cgu.PathObject {
|
||||
pathObj.Alpha = mergeAlpha(pathObj.Alpha, cgu.Alpha)
|
||||
r.renderPath(ctx, pathObj, pageH, defaultFill, defaultStroke, defaultLW, ¤tCTM, boundaryInCTM)
|
||||
}
|
||||
for _, textObj := range cgu.TextObject {
|
||||
textObj.Alpha = mergeAlpha(textObj.Alpha, cgu.Alpha)
|
||||
r.renderText(ctx, textObj, pageH, defaultFill, defaultStroke, ¤tCTM, boundaryInCTM)
|
||||
}
|
||||
for _, subCgu := range cgu.CompositeGraphicUnit {
|
||||
subCgu.Alpha = mergeAlpha(subCgu.Alpha, cgu.Alpha)
|
||||
r.renderCompositeGraphicUnit(ctx, subCgu, pageH, defaultFill, defaultStroke, defaultLW, ¤tCTM, boundaryInCTM)
|
||||
}
|
||||
ctx.Pop()
|
||||
}
|
||||
|
||||
// renderObject 渲染图形对象
|
||||
// 入参: ctx 画布上下文, obj 图形对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换
|
||||
func (r *Renderer) renderObject(ctx *canvas.Context, obj GraphicObject, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix, boundaryInCTM bool) {
|
||||
switch obj.Type {
|
||||
case "TextObject":
|
||||
r.renderText(ctx, obj.TextObject, pageH, defaultFill, defaultStroke, parentCTM, boundaryInCTM)
|
||||
case "PathObject":
|
||||
r.renderPath(ctx, obj.PathObject, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, boundaryInCTM)
|
||||
case "ImageObject":
|
||||
r.renderImage(ctx, obj.ImageObject, pageH, parentCTM, boundaryInCTM)
|
||||
case "CompositeGraphicUnit", "CompositeObject":
|
||||
r.renderCompositeGraphicUnit(ctx, obj.CompositeGraphicUnit, pageH, defaultFill, defaultStroke, defaultLW, parentCTM, boundaryInCTM)
|
||||
}
|
||||
}
|
||||
|
||||
// mergeGraphicObjectAlpha 合并图形对象透明度
|
||||
// 入参: obj 图形对象, alpha 父级透明度
|
||||
// 返回: GraphicObject 合并后的图形对象
|
||||
func mergeGraphicObjectAlpha(obj GraphicObject, alpha *int) GraphicObject {
|
||||
if alpha == nil {
|
||||
return obj
|
||||
}
|
||||
switch obj.Type {
|
||||
case "TextObject":
|
||||
obj.TextObject.Alpha = mergeAlpha(obj.TextObject.Alpha, alpha)
|
||||
case "PathObject":
|
||||
obj.PathObject.Alpha = mergeAlpha(obj.PathObject.Alpha, alpha)
|
||||
case "ImageObject":
|
||||
obj.ImageObject.Alpha = mergeAlpha(obj.ImageObject.Alpha, alpha)
|
||||
case "CompositeGraphicUnit", "CompositeObject":
|
||||
obj.CompositeGraphicUnit.Alpha = mergeAlpha(obj.CompositeGraphicUnit.Alpha, alpha)
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
// drawParamDefaults 合并绘制参数默认样式
|
||||
// 入参: id 绘制参数ID, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽
|
||||
// 返回: color.Color 默认填充色, color.Color 默认描边色, float64 默认线宽
|
||||
func (r *Renderer) drawParamDefaults(id string, defaultFill, defaultStroke color.Color, defaultLW float64) (color.Color, color.Color, float64) {
|
||||
if id == "" {
|
||||
return defaultFill, defaultStroke, defaultLW
|
||||
}
|
||||
dp := r.getDrawParam(id, nil)
|
||||
if dp == nil {
|
||||
return defaultFill, defaultStroke, defaultLW
|
||||
}
|
||||
if dp.LineWidth > 0 {
|
||||
defaultLW = dp.LineWidth
|
||||
}
|
||||
if dp.FillColor != nil {
|
||||
defaultFill = parseFillColor(dp.FillColor)
|
||||
}
|
||||
if dp.StrokeColor != nil {
|
||||
defaultStroke = parseStrokeColor(dp.StrokeColor)
|
||||
}
|
||||
return defaultFill, defaultStroke, defaultLW
|
||||
}
|
||||
|
||||
// getDrawParam 获取绘制参数逻辑
|
||||
// 入参: id 参数ID, visited 访问记录
|
||||
// 返回: *DrawParam 绘制参数
|
||||
func (r *Renderer) getDrawParam(id string, visited map[string]bool) *DrawParam {
|
||||
if visited == nil {
|
||||
visited = make(map[string]bool)
|
||||
}
|
||||
if visited[id] {
|
||||
return nil
|
||||
}
|
||||
visited[id] = true
|
||||
if dp, ok := r.DrawParams[id]; ok {
|
||||
if dp.Relative != "" {
|
||||
base := r.getDrawParam(dp.Relative, visited)
|
||||
if base == nil {
|
||||
return dp
|
||||
}
|
||||
merged := *base
|
||||
if dp.LineWidth > 0 {
|
||||
merged.LineWidth = dp.LineWidth
|
||||
}
|
||||
if dp.Join != "" {
|
||||
merged.Join = dp.Join
|
||||
}
|
||||
if dp.Cap != "" {
|
||||
merged.Cap = dp.Cap
|
||||
}
|
||||
if dp.DashPattern != "" {
|
||||
merged.DashPattern = dp.DashPattern
|
||||
merged.DashOffset = dp.DashOffset
|
||||
}
|
||||
if dp.MiterLimit > 0 {
|
||||
merged.MiterLimit = dp.MiterLimit
|
||||
}
|
||||
if dp.FillColor != nil {
|
||||
merged.FillColor = dp.FillColor
|
||||
}
|
||||
if dp.StrokeColor != nil {
|
||||
merged.StrokeColor = dp.StrokeColor
|
||||
}
|
||||
if dp.Font != "" {
|
||||
merged.Font = dp.Font
|
||||
}
|
||||
if dp.Size > 0 {
|
||||
merged.Size = dp.Size
|
||||
}
|
||||
if dp.Weight > 0 {
|
||||
merged.Weight = dp.Weight
|
||||
}
|
||||
if dp.Italic {
|
||||
merged.Italic = dp.Italic
|
||||
}
|
||||
return &merged
|
||||
}
|
||||
return dp
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// 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"
|
||||
"io"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
"github.com/tdewolff/canvas/renderers"
|
||||
"github.com/tdewolff/canvas/renderers/pdf"
|
||||
"github.com/tdewolff/canvas/renderers/rasterizer"
|
||||
)
|
||||
|
||||
// RenderToImage 渲染为光栅图
|
||||
// 入参: page 页面内容
|
||||
// 返回: image.Image 图像对象, error 错误信息
|
||||
func (r *Renderer) RenderToImage(page *PageContent) (image.Image, error) {
|
||||
c, err := r.RenderPage(page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dpmm := r.DPI / 25.4
|
||||
return rasterizer.Draw(c, canvas.DPMM(dpmm), canvas.DefaultColorSpace), nil
|
||||
}
|
||||
|
||||
// RenderToSVG 渲染为SVG
|
||||
// 入参: page 页面内容, writer 输出流
|
||||
// 返回: error 错误信息
|
||||
func (r *Renderer) RenderToSVG(page *PageContent, writer io.Writer) error {
|
||||
c, err := r.RenderPage(page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Write(writer, renderers.SVG())
|
||||
}
|
||||
|
||||
// replacePDFProducer 替换PDF的Producer属性
|
||||
// 入参: data PDF字节数据
|
||||
// 返回: []byte 替换后的PDF字节数据
|
||||
func replacePDFProducer(data []byte) []byte {
|
||||
old := []byte("/Producer(tdewolff/canvas)")
|
||||
idx := bytes.LastIndex(data, old)
|
||||
if idx < 0 {
|
||||
return data
|
||||
}
|
||||
dst := []byte("/Producer(xiaoqidun/ofdgo)")
|
||||
result := make([]byte, 0, len(data)-len(old)+len(dst))
|
||||
return append(append(append(result, data[:idx]...), dst...), data[idx+len(old):]...)
|
||||
}
|
||||
|
||||
// RenderToPDF 渲染为PDF
|
||||
// 入参: page 页面内容, writer 输出流
|
||||
// 返回: error 错误信息
|
||||
func (r *Renderer) RenderToPDF(page *PageContent, writer io.Writer) error {
|
||||
c, err := r.RenderPage(page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
p := pdf.New(&buf, c.W, c.H, nil)
|
||||
p.SetInfo("", "", "", "", "xiaoqidun/ofdgo")
|
||||
c.RenderTo(p)
|
||||
if err := p.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = writer.Write(replacePDFProducer(buf.Bytes()))
|
||||
return err
|
||||
}
|
||||
|
||||
// RenderToEPS 渲染为EPS
|
||||
// 入参: page 页面内容, writer 输出流
|
||||
// 返回: error 错误信息
|
||||
func (r *Renderer) RenderToEPS(page *PageContent, writer io.Writer) error {
|
||||
c, err := r.RenderPage(page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Write(writer, renderers.EPS())
|
||||
}
|
||||
|
||||
// RenderToMultiPagePDF 将整个文档导出为多页PDF
|
||||
// 入参: writer 输出流
|
||||
// 返回: error 错误信息
|
||||
func (r *Renderer) RenderToMultiPagePDF(writer io.Writer) error {
|
||||
doc, err := r.Reader.Doc()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(doc.Pages.Page) == 0 {
|
||||
return fmt.Errorf("no pages found")
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
var p *pdf.PDF
|
||||
for _, pgRef := range doc.Pages.Page {
|
||||
page, err := r.Reader.PageContent(pgRef)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
c, err := r.RenderPage(page)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if p == nil {
|
||||
p = pdf.New(&buf, c.W, c.H, nil)
|
||||
p.SetInfo("", "", "", "", "xiaoqidun/ofdgo")
|
||||
} else {
|
||||
p.NewPage(c.W, c.H)
|
||||
}
|
||||
c.RenderTo(p)
|
||||
}
|
||||
if p == nil {
|
||||
return fmt.Errorf("failed to render any page")
|
||||
}
|
||||
if err := p.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = writer.Write(replacePDFProducer(buf.Bytes()))
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,481 @@
|
||||
// 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"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
const defaultPathLineWidth = 0.353
|
||||
|
||||
type pathStyle struct {
|
||||
fillColor color.Color
|
||||
strokeColor color.Color
|
||||
fillPaint any
|
||||
strokePaint any
|
||||
fillPattern *Pattern
|
||||
fillPatternColor color.Color
|
||||
lineWidth float64
|
||||
lineCap canvas.Capper
|
||||
lineJoin canvas.Joiner
|
||||
dashOffset float64
|
||||
dashPattern []float64
|
||||
}
|
||||
|
||||
// newPathStyle 创建路径样式
|
||||
// 入参: defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, alpha 对象透明度
|
||||
// 返回: pathStyle 路径样式
|
||||
func newPathStyle(defaultFill, defaultStroke color.Color, defaultLW float64, alpha *int) pathStyle {
|
||||
style := pathStyle{
|
||||
fillColor: colorWithAlpha(defaultFill, alpha),
|
||||
strokeColor: colorWithAlpha(defaultStroke, alpha),
|
||||
lineWidth: defaultLW,
|
||||
lineCap: canvas.ButtCap,
|
||||
lineJoin: canvas.MiterJoin,
|
||||
}
|
||||
if style.lineWidth == 0 {
|
||||
style.lineWidth = defaultPathLineWidth
|
||||
}
|
||||
style.fillPaint = style.fillColor
|
||||
style.strokePaint = style.strokeColor
|
||||
return style
|
||||
}
|
||||
|
||||
// applyFillColor 应用填充颜色
|
||||
// 入参: fill 填充颜色, bx 边界X坐标, by 边界Y坐标, pageH 页面高度, alpha 对象透明度
|
||||
func (s *pathStyle) applyFillColor(fill *FillColor, bx, by, pageH float64, alpha *int) {
|
||||
fillColorNode := withFillAlpha(fill, alpha)
|
||||
s.fillPattern = fillColorNode.Pattern
|
||||
s.fillPatternColor = patternColor(fillColorNode)
|
||||
s.fillColor = parseFillColor(fillColorNode)
|
||||
s.fillPaint = parseFillPaint(fillColorNode, bx, by, pageH, 0, 0)
|
||||
}
|
||||
|
||||
// applyStrokeColor 应用描边颜色
|
||||
// 入参: stroke 描边颜色, bx 边界X坐标, by 边界Y坐标, pageH 页面高度, alpha 对象透明度
|
||||
func (s *pathStyle) applyStrokeColor(stroke *StrokeColor, bx, by, pageH float64, alpha *int) {
|
||||
strokeColorNode := withStrokeAlpha(stroke, alpha)
|
||||
s.strokeColor = parseStrokeColor(strokeColorNode)
|
||||
s.strokePaint = parseStrokePaint(strokeColorNode, bx, by, pageH, 0, 0)
|
||||
}
|
||||
|
||||
// pathLineCap 转换线帽样式
|
||||
// 入参: cap 线帽名称, fallback 默认线帽
|
||||
// 返回: canvas.Capper 线帽样式
|
||||
func pathLineCap(cap string, fallback canvas.Capper) canvas.Capper {
|
||||
switch cap {
|
||||
case "Round":
|
||||
return canvas.RoundCap
|
||||
case "Square":
|
||||
return canvas.SquareCap
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// pathLineJoin 转换线连接样式
|
||||
// 入参: join 线连接名称, fallback 默认线连接
|
||||
// 返回: canvas.Joiner 线连接样式
|
||||
func pathLineJoin(join string, fallback canvas.Joiner) canvas.Joiner {
|
||||
switch join {
|
||||
case "Round":
|
||||
return canvas.RoundJoin
|
||||
case "Bevel":
|
||||
return canvas.BevelJoin
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// applyDrawParam 应用绘制参数样式
|
||||
// 入参: dp 绘制参数, bx 边界X坐标, by 边界Y坐标, pageH 页面高度, alpha 对象透明度
|
||||
func (s *pathStyle) applyDrawParam(dp *DrawParam, bx, by, pageH float64, alpha *int) {
|
||||
if dp.LineWidth > 0 {
|
||||
s.lineWidth = dp.LineWidth
|
||||
}
|
||||
if dp.FillColor != nil {
|
||||
s.applyFillColor(dp.FillColor, bx, by, pageH, alpha)
|
||||
}
|
||||
if dp.StrokeColor != nil {
|
||||
s.applyStrokeColor(dp.StrokeColor, bx, by, pageH, alpha)
|
||||
}
|
||||
if dp.Cap != "" {
|
||||
s.lineCap = pathLineCap(dp.Cap, s.lineCap)
|
||||
}
|
||||
if dp.Join != "" {
|
||||
s.lineJoin = pathLineJoin(dp.Join, s.lineJoin)
|
||||
}
|
||||
if dp.DashPattern != "" {
|
||||
s.dashPattern = parseFloats(dp.DashPattern)
|
||||
s.dashOffset = dp.DashOffset
|
||||
}
|
||||
}
|
||||
|
||||
// applyPathObject 应用路径对象样式
|
||||
// 入参: obj 路径对象, bx 边界X坐标, by 边界Y坐标, pageH 页面高度
|
||||
func (s *pathStyle) applyPathObject(obj PathObject, bx, by, pageH float64) {
|
||||
if obj.LineWidth > 0 {
|
||||
s.lineWidth = obj.LineWidth
|
||||
}
|
||||
if obj.FillColor != nil {
|
||||
s.applyFillColor(obj.FillColor, bx, by, pageH, obj.Alpha)
|
||||
}
|
||||
if obj.StrokeColor != nil {
|
||||
s.applyStrokeColor(obj.StrokeColor, bx, by, pageH, obj.Alpha)
|
||||
}
|
||||
if obj.Cap != "" {
|
||||
s.lineCap = pathLineCap(obj.Cap, canvas.ButtCap)
|
||||
}
|
||||
if obj.Join != "" {
|
||||
s.lineJoin = pathLineJoin(obj.Join, canvas.MiterJoin)
|
||||
}
|
||||
if obj.DashPattern != "" {
|
||||
s.dashPattern = parseFloats(obj.DashPattern)
|
||||
s.dashOffset = obj.DashOffset
|
||||
}
|
||||
}
|
||||
|
||||
// scale 应用路径变换缩放
|
||||
// 入参: ctm 变换矩阵
|
||||
func (s *pathStyle) scale(ctm Matrix) {
|
||||
if scale := math.Sqrt(math.Abs(ctm.a*ctm.d - ctm.b*ctm.c)); scale > 0 {
|
||||
s.lineWidth *= scale
|
||||
s.dashOffset *= scale
|
||||
for i := range s.dashPattern {
|
||||
s.dashPattern[i] *= scale
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// renderPath 渲染路径
|
||||
// 入参: ctx 画布上下文, obj 路径对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换
|
||||
func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix, boundaryInCTM bool) {
|
||||
if obj.Visible != nil && !*obj.Visible {
|
||||
return
|
||||
}
|
||||
ctx.Push()
|
||||
bx, by := 0.0, 0.0
|
||||
if obj.Boundary != "" {
|
||||
if box, err := ParseBox(obj.Boundary); err == nil {
|
||||
bx, by = box.X, box.Y
|
||||
}
|
||||
}
|
||||
ctm := NewMatrix(obj.CTM)
|
||||
if parentCTM != nil {
|
||||
ctm = parentCTM.Multiply(ctm)
|
||||
}
|
||||
style := newPathStyle(defaultFill, defaultStroke, defaultLW, obj.Alpha)
|
||||
if obj.DrawParam != "" {
|
||||
if dp := r.getDrawParam(obj.DrawParam, nil); dp != nil {
|
||||
style.applyDrawParam(dp, bx, by, pageH, obj.Alpha)
|
||||
}
|
||||
}
|
||||
style.applyPathObject(obj, bx, by, pageH)
|
||||
style.scale(ctm)
|
||||
p := r.buildPath(obj, pageH, ctm, boundaryInCTM)
|
||||
if rectPath := r.buildTinyFillRectPath(obj, pageH, ctm, bx, by); rectPath != nil {
|
||||
p = rectPath
|
||||
}
|
||||
clipPath := r.buildClipPath(obj.Clips, pageH, bx, by, ctm)
|
||||
shouldFill := false
|
||||
if obj.Fill != nil {
|
||||
shouldFill = *obj.Fill
|
||||
}
|
||||
if style.fillPaint == nil {
|
||||
style.fillPaint = style.fillColor
|
||||
}
|
||||
if shouldFill && style.fillPattern != nil {
|
||||
fp := p
|
||||
if clipPath != nil {
|
||||
fp = p.Copy()
|
||||
fp.Close()
|
||||
fp = fp.And(clipPath)
|
||||
}
|
||||
r.renderPattern(ctx, style.fillPattern, style.fillPatternColor, pageH, fp, ctm, bx, by)
|
||||
} else if shouldFill && style.fillPaint != nil {
|
||||
ctx.SetFill(style.fillPaint)
|
||||
ctx.SetStrokeColor(canvas.Transparent)
|
||||
fp := p
|
||||
if clipPath != nil {
|
||||
fp = p.Copy()
|
||||
fp.Close()
|
||||
fp = fp.And(clipPath)
|
||||
}
|
||||
ctx.DrawPath(0, 0, fp)
|
||||
}
|
||||
shouldStroke := true
|
||||
if obj.Stroke != nil {
|
||||
shouldStroke = *obj.Stroke
|
||||
}
|
||||
if shouldStroke {
|
||||
if style.strokePaint == nil {
|
||||
style.strokePaint = style.strokeColor
|
||||
}
|
||||
if style.strokePaint == nil {
|
||||
style.strokePaint = colorWithAlpha(canvas.Black, obj.Alpha)
|
||||
}
|
||||
ctx.SetFillColor(canvas.Transparent)
|
||||
ctx.SetStroke(style.strokePaint)
|
||||
ctx.SetStrokeWidth(style.lineWidth)
|
||||
ctx.SetStrokeCapper(style.lineCap)
|
||||
ctx.SetStrokeJoiner(style.lineJoin)
|
||||
if len(style.dashPattern) > 0 {
|
||||
ctx.SetDashes(style.dashOffset, style.dashPattern...)
|
||||
}
|
||||
if clipPath != nil {
|
||||
sp := p.Copy()
|
||||
if len(style.dashPattern) > 0 {
|
||||
sp = sp.Dash(style.dashOffset, style.dashPattern...)
|
||||
}
|
||||
sp = sp.Stroke(style.lineWidth, style.lineCap, style.lineJoin, canvas.Tolerance)
|
||||
sp = sp.And(clipPath)
|
||||
ctx.SetFill(style.strokePaint)
|
||||
ctx.SetStrokeColor(canvas.Transparent)
|
||||
ctx.DrawPath(0, 0, sp)
|
||||
} else {
|
||||
ctx.DrawPath(0, 0, p)
|
||||
}
|
||||
}
|
||||
ctx.Pop()
|
||||
}
|
||||
|
||||
// renderPattern 渲染图案填充
|
||||
// 入参: ctx 画布上下文, pattern 图案对象, defaultColor 默认颜色, pageH 页面高度, clip 填充区域, parentCTM 父级CTM, bx 边界X坐标, by 边界Y坐标
|
||||
func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *Pattern, defaultColor color.Color, pageH float64, clip *canvas.Path, parentCTM Matrix, bx, by float64) {
|
||||
if pattern == nil || clip == nil || len(pattern.CellContent.Objects) == 0 {
|
||||
return
|
||||
}
|
||||
xStep, yStep := pattern.XStep, pattern.YStep
|
||||
if xStep == 0 {
|
||||
xStep = pattern.Width
|
||||
}
|
||||
if yStep == 0 {
|
||||
yStep = pattern.Height
|
||||
}
|
||||
if xStep <= 0 || yStep <= 0 {
|
||||
return
|
||||
}
|
||||
patternCTM := TranslationMatrix(bx, by).Multiply(parentCTM).Multiply(NewMatrix(pattern.CTM))
|
||||
invCTM, ok := patternCTM.Invert()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
bounds := clip.FastBounds()
|
||||
points := [][2]float64{
|
||||
{bounds.X0, pageH - bounds.Y0},
|
||||
{bounds.X1, pageH - bounds.Y0},
|
||||
{bounds.X1, pageH - bounds.Y1},
|
||||
{bounds.X0, pageH - bounds.Y1},
|
||||
}
|
||||
minX, maxX := 0.0, 0.0
|
||||
minY, maxY := 0.0, 0.0
|
||||
for i, point := range points {
|
||||
x, y := invCTM.Transform(point[0], point[1])
|
||||
if i == 0 {
|
||||
minX, maxX = x, x
|
||||
minY, maxY = y, y
|
||||
continue
|
||||
}
|
||||
minX = math.Min(minX, x)
|
||||
maxX = math.Max(maxX, x)
|
||||
minY = math.Min(minY, y)
|
||||
maxY = math.Max(maxY, y)
|
||||
}
|
||||
startX := int(math.Floor(minX/xStep)) - 1
|
||||
endX := int(math.Ceil(maxX/xStep)) + 1
|
||||
startY := int(math.Floor(minY/yStep)) - 1
|
||||
endY := int(math.Ceil(maxY/yStep)) + 1
|
||||
for ix := startX; ix <= endX; ix++ {
|
||||
for iy := startY; iy <= endY; iy++ {
|
||||
tileCTM := patternCTM.Multiply(TranslationMatrix(float64(ix)*xStep, float64(iy)*yStep))
|
||||
for _, obj := range pattern.CellContent.Objects {
|
||||
r.renderObject(ctx, obj, pageH, defaultColor, defaultColor, 0, &tileCTM, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// buildPath 解析路径并返回Canvas Path
|
||||
// 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵, boundaryInCTM 边界是否参与CTM变换
|
||||
// 返回: *canvas.Path 路径对象
|
||||
func (r *Renderer) buildPath(obj PathObject, pageH float64, ctm Matrix, boundaryInCTM bool) *canvas.Path {
|
||||
bx, by := 0.0, 0.0
|
||||
if obj.Boundary != "" {
|
||||
if box, err := ParseBox(obj.Boundary); err == nil {
|
||||
bx, by = box.X, box.Y
|
||||
}
|
||||
}
|
||||
point := func(x, y float64) (float64, float64) {
|
||||
if boundaryInCTM {
|
||||
tx, ty := ctm.Transform(x+bx, y+by)
|
||||
return tx, pageH - ty
|
||||
}
|
||||
tx, ty := ctm.Transform(x, y)
|
||||
return tx + bx, pageH - (ty + by)
|
||||
}
|
||||
p := &canvas.Path{}
|
||||
tokens := strings.Fields(obj.AbbreviatedData)
|
||||
for i := 0; i < len(tokens); {
|
||||
cmd := tokens[i]
|
||||
i++
|
||||
switch cmd {
|
||||
case "M", "S":
|
||||
if i+1 < len(tokens) {
|
||||
x, _ := strconv.ParseFloat(tokens[i], 64)
|
||||
y, _ := strconv.ParseFloat(tokens[i+1], 64)
|
||||
tx, ty := point(x, y)
|
||||
p.MoveTo(tx, ty)
|
||||
i += 2
|
||||
}
|
||||
case "L":
|
||||
if i+1 < len(tokens) {
|
||||
x, _ := strconv.ParseFloat(tokens[i], 64)
|
||||
y, _ := strconv.ParseFloat(tokens[i+1], 64)
|
||||
tx, ty := point(x, y)
|
||||
p.LineTo(tx, ty)
|
||||
i += 2
|
||||
}
|
||||
case "B":
|
||||
if i+5 < len(tokens) {
|
||||
x1, _ := strconv.ParseFloat(tokens[i], 64)
|
||||
y1, _ := strconv.ParseFloat(tokens[i+1], 64)
|
||||
x2, _ := strconv.ParseFloat(tokens[i+2], 64)
|
||||
y2, _ := strconv.ParseFloat(tokens[i+3], 64)
|
||||
x3, _ := strconv.ParseFloat(tokens[i+4], 64)
|
||||
y3, _ := strconv.ParseFloat(tokens[i+5], 64)
|
||||
tx1, ty1 := point(x1, y1)
|
||||
tx2, ty2 := point(x2, y2)
|
||||
tx3, ty3 := point(x3, y3)
|
||||
p.CubeTo(tx1, ty1, tx2, ty2, tx3, ty3)
|
||||
i += 6
|
||||
}
|
||||
case "Q":
|
||||
if i+3 < len(tokens) {
|
||||
x1, _ := strconv.ParseFloat(tokens[i], 64)
|
||||
y1, _ := strconv.ParseFloat(tokens[i+1], 64)
|
||||
x2, _ := strconv.ParseFloat(tokens[i+2], 64)
|
||||
y2, _ := strconv.ParseFloat(tokens[i+3], 64)
|
||||
tx1, ty1 := point(x1, y1)
|
||||
tx2, ty2 := point(x2, y2)
|
||||
p.QuadTo(tx1, ty1, tx2, ty2)
|
||||
i += 4
|
||||
}
|
||||
case "A":
|
||||
if i+6 < len(tokens) {
|
||||
rx, _ := strconv.ParseFloat(tokens[i], 64)
|
||||
ry, _ := strconv.ParseFloat(tokens[i+1], 64)
|
||||
rot, _ := strconv.ParseFloat(tokens[i+2], 64)
|
||||
large, _ := strconv.ParseBool(tokens[i+3])
|
||||
sweep, _ := strconv.ParseBool(tokens[i+4])
|
||||
x, _ := strconv.ParseFloat(tokens[i+5], 64)
|
||||
y, _ := strconv.ParseFloat(tokens[i+6], 64)
|
||||
sx := math.Hypot(ctm.a, ctm.c)
|
||||
sy := math.Hypot(ctm.b, ctm.d)
|
||||
ctmRot := math.Atan2(ctm.b, ctm.a) * 180 / math.Pi
|
||||
tx, ty := point(x, y)
|
||||
sweep = !sweep
|
||||
p.ArcTo(rx*sx, ry*sy, -(rot + ctmRot), large, sweep, tx, ty)
|
||||
i += 7
|
||||
}
|
||||
case "C":
|
||||
p.Close()
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// buildClipPath 构建裁剪路径
|
||||
// 入参: clips 裁剪对象, pageH 页面高度, bx 边界X坐标, by 边界Y坐标, objectCTM 对象CTM
|
||||
// 返回: *canvas.Path 路径对象
|
||||
func (r *Renderer) buildClipPath(clips *Clips, pageH float64, bx, by float64, objectCTM Matrix) *canvas.Path {
|
||||
if clips == nil {
|
||||
return nil
|
||||
}
|
||||
var p *canvas.Path
|
||||
for _, clip := range clips.Clip {
|
||||
var clipPath *canvas.Path
|
||||
for _, area := range clip.Area {
|
||||
areaCTM := NewMatrix(area.CTM)
|
||||
if clips.TransFlag {
|
||||
areaCTM = objectCTM.Multiply(areaCTM)
|
||||
}
|
||||
for _, pathObj := range area.Path {
|
||||
ctm := areaCTM.Multiply(NewMatrix(pathObj.CTM))
|
||||
cp := r.buildPath(pathObj, pageH, ctm, true)
|
||||
cp.Translate(bx, -by)
|
||||
cp.Close()
|
||||
if clipPath == nil {
|
||||
clipPath = cp
|
||||
} else {
|
||||
clipPath = clipPath.Or(cp)
|
||||
}
|
||||
}
|
||||
}
|
||||
if clipPath != nil {
|
||||
if p == nil {
|
||||
p = clipPath
|
||||
} else {
|
||||
p = p.And(clipPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// buildTinyFillRectPath 构建微小填充矩形路径
|
||||
// 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵, bx 边界X坐标, by 边界Y坐标
|
||||
// 返回: *canvas.Path 路径对象
|
||||
func (r *Renderer) buildTinyFillRectPath(obj PathObject, pageH float64, ctm Matrix, bx, by float64) *canvas.Path {
|
||||
if obj.Fill == nil || !*obj.Fill || obj.Stroke == nil || *obj.Stroke {
|
||||
return nil
|
||||
}
|
||||
box, err := ParseBox(obj.Boundary)
|
||||
if err != nil || box.W <= 0 || box.H <= 0 || box.W > 0.6 || box.H > 0.6 {
|
||||
return nil
|
||||
}
|
||||
tokens := strings.Fields(obj.AbbreviatedData)
|
||||
if len(tokens) != 11 || tokens[0] != "M" || tokens[3] != "L" || tokens[6] != "L" || tokens[9] != "L" || tokens[10] != "C" {
|
||||
return nil
|
||||
}
|
||||
points := make([][2]float64, 0, 4)
|
||||
for i := 1; i < 10; i += 3 {
|
||||
x, errX := strconv.ParseFloat(tokens[i], 64)
|
||||
y, errY := strconv.ParseFloat(tokens[i+1], 64)
|
||||
if errX != nil || errY != nil {
|
||||
return nil
|
||||
}
|
||||
tx, ty := ctm.Transform(x, y)
|
||||
points = append(points, [2]float64{tx + bx, pageH - (ty + by)})
|
||||
}
|
||||
minX, maxX := points[0][0], points[0][0]
|
||||
minY, maxY := points[0][1], points[0][1]
|
||||
for _, point := range points[1:] {
|
||||
minX = math.Min(minX, point[0])
|
||||
maxX = math.Max(maxX, point[0])
|
||||
minY = math.Min(minY, point[1])
|
||||
maxY = math.Max(maxY, point[1])
|
||||
}
|
||||
expand := math.Min(box.W, box.H) * 0.08
|
||||
p := &canvas.Path{}
|
||||
p.MoveTo(minX-expand, minY-expand)
|
||||
p.LineTo(maxX+expand, minY-expand)
|
||||
p.LineTo(maxX+expand, maxY+expand)
|
||||
p.LineTo(minX-expand, maxY+expand)
|
||||
p.Close()
|
||||
return p
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// 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"
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
// renderStamp 渲染印章
|
||||
// 入参: ctx 画布上下文, s 印章对象, pageH 页面高度
|
||||
func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) {
|
||||
x, y, w, h := s.Box.X, s.Box.Y, s.Box.W, s.Box.H
|
||||
screenY := pageH - (y + h)
|
||||
if s.Type == "ofd" && len(s.Data) > 0 {
|
||||
reader, err := NewReader(bytes.NewReader(s.Data), int64(len(s.Data)))
|
||||
if err == nil {
|
||||
defer reader.Close()
|
||||
doc, err := reader.Doc()
|
||||
if err == nil {
|
||||
renderer := r.childRenderer(reader)
|
||||
for _, pageRef := range doc.Pages.Page {
|
||||
content, err := reader.PageContent(pageRef)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
sealBox, err := renderer.GetPageBox(content)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ctx.Push()
|
||||
ctx.Translate(x, screenY)
|
||||
ctx.Scale(w/sealBox.W, h/sealBox.H)
|
||||
renderer.renderPageToContext(ctx, content, false)
|
||||
ctx.Pop()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(s.Data) > 0 {
|
||||
img, _, err := decodeImageData(s.Data)
|
||||
if err == nil {
|
||||
img = stampImageWithTransparentWhite(img)
|
||||
ctx.Push()
|
||||
ctx.Translate(x, screenY)
|
||||
ctx.Scale(w/float64(img.Bounds().Dx()), h/float64(img.Bounds().Dy()))
|
||||
ctx.DrawImage(0, 0, img, canvas.DPMM(1.0))
|
||||
ctx.Pop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stampImageWithTransparentWhite 处理印章图片白色底色
|
||||
// 入参: img 印章图片对象
|
||||
// 返回: image.Image 处理后的印章图片对象
|
||||
func stampImageWithTransparentWhite(img image.Image) image.Image {
|
||||
if opaque, ok := img.(interface{ Opaque() bool }); ok && !opaque.Opaque() {
|
||||
return img
|
||||
}
|
||||
bounds := img.Bounds()
|
||||
out := image.NewNRGBA(bounds)
|
||||
hasAlpha := false
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
|
||||
if c.A < 255 {
|
||||
hasAlpha = true
|
||||
}
|
||||
out.SetNRGBA(x, y, c)
|
||||
}
|
||||
}
|
||||
if hasAlpha {
|
||||
return img
|
||||
}
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
c := out.NRGBAAt(x, y)
|
||||
if c.R >= 250 && c.G >= 250 && c.B >= 250 {
|
||||
c.A = 0
|
||||
out.SetNRGBA(x, y, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
// 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"
|
||||
"strings"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
const ptPerMM = 72.0 / 25.4
|
||||
|
||||
// renderText 渲染文本
|
||||
// 入参: ctx 画布上下文, obj 文本对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM
|
||||
func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64, defaultFill, defaultStroke color.Color, parentCTM *Matrix, boundaryInCTM bool) {
|
||||
if obj.Visible != nil && !*obj.Visible {
|
||||
return
|
||||
}
|
||||
ctx.Push()
|
||||
bx, by := 0.0, 0.0
|
||||
if obj.Boundary != "" {
|
||||
if box, err := ParseBox(obj.Boundary); err == nil {
|
||||
bx, by = box.X, box.Y
|
||||
}
|
||||
}
|
||||
localCTM := NewMatrix(obj.CTM)
|
||||
ctm := localCTM
|
||||
if parentCTM != nil {
|
||||
ctm = parentCTM.Multiply(ctm)
|
||||
}
|
||||
var dp *DrawParam
|
||||
if obj.DrawParam != "" {
|
||||
dp = r.getDrawParam(obj.DrawParam, nil)
|
||||
}
|
||||
sizeMM := obj.Size
|
||||
if sizeMM == 0 && dp != nil && dp.Size > 0 {
|
||||
sizeMM = dp.Size
|
||||
}
|
||||
if sizeMM == 0 {
|
||||
sizeMM = 3.5
|
||||
}
|
||||
if obj.VScale != 0 {
|
||||
sizeMM *= obj.VScale
|
||||
}
|
||||
hScale := obj.HScale
|
||||
if hScale == 0 {
|
||||
hScale = 1
|
||||
}
|
||||
useTextMatrix := hasTextMatrix(ctm)
|
||||
if scale := ctm.YScale(); scale > 0 && !useTextMatrix {
|
||||
sizeMM *= scale
|
||||
}
|
||||
sizePt := sizeMM * ptPerMM
|
||||
fillColor := colorWithAlpha(defaultFill, obj.Alpha)
|
||||
if fillColor == nil {
|
||||
fillColor = colorWithAlpha(canvas.Black, obj.Alpha)
|
||||
}
|
||||
var fillPaint any = fillColor
|
||||
var fillColorNode *FillColor
|
||||
if dp != nil && dp.FillColor != nil {
|
||||
fillColorNode = withFillAlpha(dp.FillColor, obj.Alpha)
|
||||
fillColor = parseFillColor(fillColorNode)
|
||||
fillPaint = parseFillPaint(fillColorNode, bx, by, pageH, 0, 0)
|
||||
}
|
||||
if obj.FillColor != nil {
|
||||
fillColorNode = withFillAlpha(obj.FillColor, obj.Alpha)
|
||||
fillColor = parseFillColor(fillColorNode)
|
||||
fillPaint = parseFillPaint(fillColorNode, bx, by, pageH, 0, 0)
|
||||
}
|
||||
if fillPaint == nil {
|
||||
fillPaint = fillColor
|
||||
}
|
||||
fontStyle := canvas.FontRegular
|
||||
weight := obj.Weight
|
||||
if weight == 0 && dp != nil && dp.Weight > 0 {
|
||||
weight = dp.Weight
|
||||
}
|
||||
syntheticBold := weight >= 700
|
||||
italic := obj.Italic
|
||||
if !italic && dp != nil && dp.Italic {
|
||||
italic = true
|
||||
}
|
||||
if italic {
|
||||
fontStyle |= canvas.FontItalic
|
||||
}
|
||||
fontID := r.textObjectFontID(obj)
|
||||
embeddedFont := false
|
||||
if of, ok := r.Reader.fontCache[fontID]; ok {
|
||||
embeddedFont = of.FontFile != ""
|
||||
if !embeddedFont && fontNoSyntheticBold(of.FontName, of.FamilyName) {
|
||||
syntheticBold = false
|
||||
}
|
||||
if of.Bold {
|
||||
fontStyle |= canvas.FontBold
|
||||
}
|
||||
if of.Italic {
|
||||
fontStyle |= canvas.FontItalic
|
||||
}
|
||||
}
|
||||
if syntheticBold {
|
||||
fontStyle |= canvas.FontBold
|
||||
}
|
||||
ff := r.loadFont(fontID)
|
||||
if ff == nil {
|
||||
return
|
||||
}
|
||||
face := ff.Face(sizePt, fillPaint, fontStyle, canvas.FontNormal)
|
||||
glyphTransforms := r.textObjectGlyphTransforms(fontID, obj)
|
||||
hasUnderline := strings.Contains(obj.Decoration, "Underline")
|
||||
useGlyphFillPaint := fillColorNode != nil && fillColorNode.AxialShd != nil
|
||||
codePos := 0
|
||||
for _, tc := range obj.TextCode {
|
||||
var runes []rune
|
||||
var glyphs []textGlyph
|
||||
if tc.Index != "" {
|
||||
runes = r.parseIndexRunes(tc.Index, fontID)
|
||||
glyphs = textRuneGlyphs(runes)
|
||||
} else {
|
||||
runes = textCodeRunes(tc.Value)
|
||||
glyphs = textCodeGlyphs(runes, glyphTransforms, codePos)
|
||||
}
|
||||
dxs, dys := parseFloats(tc.DeltaX), parseFloats(tc.DeltaY)
|
||||
xs, ys := parseFloats(tc.X), parseFloats(tc.Y)
|
||||
drawAsPath := embeddedFont || textCodePositioned(tc, xs, ys)
|
||||
cx, cy := 0.0, 0.0
|
||||
if len(xs) > 0 {
|
||||
cx = xs[0]
|
||||
}
|
||||
if len(ys) > 0 {
|
||||
cy = ys[0]
|
||||
}
|
||||
for i, glyph := range glyphs {
|
||||
str := glyph.Text
|
||||
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 += textGlyphWidth(face, glyph) * hScale
|
||||
}
|
||||
}
|
||||
if i < len(ys) {
|
||||
cy = ys[i]
|
||||
} else if i > 0 {
|
||||
if dy, ok := textDelta(dys, i-1); ok {
|
||||
cy += dy
|
||||
}
|
||||
}
|
||||
var canvasX, canvasY float64
|
||||
if boundaryInCTM && parentCTM != nil {
|
||||
tx, ty := localCTM.Transform(cx, cy)
|
||||
tx, ty = parentCTM.Transform(tx+bx, ty+by)
|
||||
canvasX, canvasY = tx, pageH-ty
|
||||
} else {
|
||||
tx, ty := ctm.Transform(cx, cy)
|
||||
canvasX, canvasY = tx+bx, pageH-(ty+by)
|
||||
}
|
||||
textWidth := textGlyphWidth(face, glyph) * hScale
|
||||
glyphFillPaint := fillPaint
|
||||
if useGlyphFillPaint {
|
||||
glyphFillPaint = parseFillPaint(fillColorNode, bx, by, pageH, canvasX, canvasY)
|
||||
}
|
||||
advanceLimit := textGlyphAdvanceLimit(dxs, dys, xs, i, len(glyphs), cx)
|
||||
if glyphFillPaint != nil {
|
||||
ctx.SetFill(glyphFillPaint)
|
||||
drawGlyph := func(x, y float64) {
|
||||
if drawAsPath || glyph.GlyphID >= 0 {
|
||||
path, width := textGlyphPath(face, glyph)
|
||||
scaleX := hScale
|
||||
if advanceLimit > 0 && width*scaleX > advanceLimit {
|
||||
scaleX = advanceLimit / width
|
||||
}
|
||||
textWidth = width * scaleX
|
||||
if scaleX != 1 {
|
||||
ctx.Push()
|
||||
ctx.Translate(x, y)
|
||||
ctx.Scale(scaleX, 1)
|
||||
ctx.DrawPath(0, 0, path)
|
||||
ctx.Pop()
|
||||
} else {
|
||||
ctx.DrawPath(x, y, path)
|
||||
}
|
||||
} else {
|
||||
scaled := hScale != 1
|
||||
if scaled {
|
||||
ctx.Push()
|
||||
ctx.Translate(x, y)
|
||||
ctx.Scale(hScale, 1)
|
||||
x, y = 0, 0
|
||||
}
|
||||
textFace := face
|
||||
if useGlyphFillPaint {
|
||||
textFace = ff.Face(sizePt, glyphFillPaint, fontStyle, canvas.FontNormal)
|
||||
}
|
||||
text := canvas.NewTextLine(textFace, str, canvas.Left)
|
||||
ctx.DrawText(x, y, text)
|
||||
if scaled {
|
||||
ctx.Pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
if useTextMatrix {
|
||||
ctx.Push()
|
||||
ctx.Translate(canvasX, canvasY)
|
||||
ctx.ComposeView(textMatrix(ctm))
|
||||
drawGlyph(0, 0)
|
||||
if hasUnderline {
|
||||
uw := sizeMM * 0.05
|
||||
ctx.SetStrokeWidth(uw)
|
||||
ctx.SetStrokeColor(fillColor)
|
||||
off := sizeMM * 0.1
|
||||
ctx.MoveTo(0, -off)
|
||||
ctx.LineTo(textWidth, -off)
|
||||
ctx.Stroke()
|
||||
}
|
||||
ctx.Pop()
|
||||
continue
|
||||
}
|
||||
drawGlyph(canvasX, canvasY)
|
||||
}
|
||||
if hasUnderline {
|
||||
uw := sizeMM * 0.05
|
||||
ctx.SetStrokeWidth(uw)
|
||||
ctx.SetStrokeColor(fillColor)
|
||||
off := sizeMM * 0.1
|
||||
ctx.MoveTo(canvasX, canvasY-off)
|
||||
ctx.LineTo(canvasX+textWidth, canvasY-off)
|
||||
ctx.Stroke()
|
||||
}
|
||||
}
|
||||
codePos += len(runes)
|
||||
}
|
||||
ctx.Pop()
|
||||
}
|
||||
Reference in New Issue
Block a user