mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
73472c5d4b
commit
77ffbe6a72
56 files changed
+2397
-1612
No files matched your search
+12
-9
@@ -43,13 +43,14 @@
|
||||
<button id="continuousButton" class="button" type="button" data-short="连" title="连续显示" aria-label="连续显示" aria-pressed="false" disabled>连续</button>
|
||||
<button id="annotationButton" class="button" type="button" data-short="注" title="隐藏注解" aria-label="隐藏注解" aria-pressed="true">注解</button>
|
||||
</div>
|
||||
<div class="tool-group">
|
||||
<div class="tool-group" role="group" aria-labelledby="exportLabel">
|
||||
<span id="exportLabel" class="tool-label">导出</span>
|
||||
<select id="exportFormat" class="format-select" title="导出格式" aria-label="导出格式" disabled></select>
|
||||
<select id="imageDPI" class="format-select dpi-select" title="图片 DPI" aria-label="图片 DPI" disabled>
|
||||
<option value="96">96</option>
|
||||
<option value="150">150</option>
|
||||
<option value="300" selected>300</option>
|
||||
<option value="600">600</option>
|
||||
<select id="imageDPI" class="format-select dpi-select" title="导出DPI(仅PNG/JPG)" aria-label="导出DPI" disabled>
|
||||
<option value="96">96 dpi</option>
|
||||
<option value="150">150 dpi</option>
|
||||
<option value="300" selected>300 dpi</option>
|
||||
<option value="600">600 dpi</option>
|
||||
</select>
|
||||
<button id="exportPageButton" class="button" type="button" data-short="单" title="导出单页" aria-label="导出单页" disabled>单页</button>
|
||||
<button id="exportButton" class="button" type="button" data-short="多" title="导出文档" aria-label="导出文档" aria-haspopup="dialog" aria-controls="exportPanel" disabled>文档</button>
|
||||
@@ -434,21 +435,23 @@
|
||||
<section id="renderPanel" class="render-panel" aria-label="渲染">
|
||||
<div class="panel-row">
|
||||
<label class="panel-title compact" for="renderBackend">渲染</label>
|
||||
<div class="render-controls">
|
||||
<select id="renderBackend" title="渲染后端" disabled></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="render-mode-row">
|
||||
<span id="renderModeLabel">显示</span>
|
||||
<span id="renderModeLabel">预览</span>
|
||||
<div class="render-controls">
|
||||
<div class="render-modes" role="group" aria-labelledby="renderModeLabel">
|
||||
<button id="renderVectorButton" class="small-button" type="button" aria-pressed="true" title="矢量显示" disabled>矢量</button>
|
||||
<button id="renderRasterButton" class="small-button" type="button" aria-pressed="false" title="位图显示" disabled>位图</button>
|
||||
</div>
|
||||
<select id="renderDPI" aria-label="精度" title="位图精度" disabled><option value="96">96 dpi</option><option value="150" selected>150 dpi</option><option value="300">300 dpi</option></select>
|
||||
<select id="renderDPI" aria-label="预览DPI" title="预览DPI(仅位图)" disabled><option value="96">96 dpi</option><option value="150" selected>150 dpi</option><option value="300">300 dpi</option><option value="600">600 dpi</option></select>
|
||||
</div>
|
||||
</div>
|
||||
<details class="render-support">
|
||||
<summary>详情</summary>
|
||||
<dl><div><dt>显示</dt><dd id="renderDisplay">SVG</dd></div><div><dt>编辑</dt><dd>SVG</dd></div><div><dt>编译</dt><dd id="renderCompiler"></dd></div><div><dt>图片</dt><dd id="renderImages"></dd></div><div><dt>SVG</dt><dd id="renderSVG"></dd></div><div><dt>PDF</dt><dd id="renderPDF"></dd></div><div><dt>EPS</dt><dd id="renderEPS"></dd></div></dl>
|
||||
<dl><div><dt>显示</dt><dd id="renderDisplay">SVG</dd></div><div><dt>编辑</dt><dd>SVG</dd></div><div><dt>字体</dt><dd id="renderFonts"></dd></div><div><dt>几何</dt><dd id="renderGeometry"></dd></div><div><dt>编译</dt><dd id="renderCompiler"></dd></div><div><dt>图片</dt><dd id="renderImages"></dd></div><div><dt>SVG</dt><dd id="renderSVG"></dd></div><div><dt>PDF</dt><dd id="renderPDF"></dd></div><div><dt>EPS</dt><dd id="renderEPS"></dd></div></dl>
|
||||
</details>
|
||||
</section>
|
||||
<div class="panel-row">
|
||||
|
||||
+17
-17
@@ -620,7 +620,12 @@ button:not(:disabled):hover {
|
||||
}
|
||||
|
||||
.dpi-select {
|
||||
width: 54px;
|
||||
width: 79px;
|
||||
}
|
||||
|
||||
.tool-label {
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.form-dialog {
|
||||
@@ -985,6 +990,8 @@ body[data-hide-meta] .meta-panel {
|
||||
}
|
||||
|
||||
.render-panel {
|
||||
--render-controls-width: 166px;
|
||||
--render-modes-width: 79px;
|
||||
position: relative;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 12px;
|
||||
@@ -992,8 +999,7 @@ body[data-hide-meta] .meta-panel {
|
||||
}
|
||||
|
||||
.render-panel select {
|
||||
width: 180px;
|
||||
max-width: calc(100% - 76px);
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
font-size: 12px;
|
||||
}
|
||||
@@ -1012,10 +1018,10 @@ body[data-hide-meta] .meta-panel {
|
||||
}
|
||||
|
||||
.render-controls {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: var(--render-modes-width) minmax(0, 1fr);
|
||||
align-items: center;
|
||||
width: 180px;
|
||||
max-width: calc(100% - 76px);
|
||||
width: var(--render-controls-width);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@@ -1025,7 +1031,7 @@ body[data-hide-meta] .meta-panel {
|
||||
}
|
||||
|
||||
.render-modes .small-button {
|
||||
width: 36px;
|
||||
width: 40px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -1041,13 +1047,6 @@ body[data-hide-meta] .meta-panel {
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
|
||||
.render-controls select {
|
||||
flex: 1;
|
||||
width: 0;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.render-support {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
@@ -1056,8 +1055,9 @@ body[data-hide-meta] .meta-panel {
|
||||
.render-support summary {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 32px;
|
||||
width: fit-content;
|
||||
right: 0;
|
||||
width: calc(var(--render-controls-width) - var(--render-modes-width) - 8px);
|
||||
text-align: center;
|
||||
line-height: 28px;
|
||||
cursor: pointer;
|
||||
color: var(--accent-strong);
|
||||
@@ -2579,6 +2579,6 @@ button.outline-link:hover {
|
||||
}
|
||||
|
||||
.dpi-select {
|
||||
width: 48px;
|
||||
width: 79px;
|
||||
}
|
||||
}
|
||||
@@ -332,6 +332,8 @@ const el = {
|
||||
renderPDF: document.querySelector("#renderPDF"),
|
||||
renderEPS: document.querySelector("#renderEPS"),
|
||||
renderCompiler: document.querySelector("#renderCompiler"),
|
||||
renderFonts: document.querySelector("#renderFonts"),
|
||||
renderGeometry: document.querySelector("#renderGeometry"),
|
||||
offlineStatus: document.querySelector("#offlineStatus"),
|
||||
refreshAppButton: document.querySelector("#refreshAppButton"),
|
||||
metaFile: document.querySelector("#metaFile"),
|
||||
@@ -3140,6 +3142,7 @@ function updateRenderBackend() {
|
||||
el.renderDPI.value = String(state.renderDPI);
|
||||
el.renderBackend.disabled = !state.ready || state.exporting || document.body.hasAttribute("aria-busy");
|
||||
el.renderDPI.disabled = el.renderBackend.disabled || displayMode() === "svg";
|
||||
el.renderDPI.title = displayMode() === "svg" ? "矢量预览无需DPI" : "预览DPI(仅位图)";
|
||||
el.renderVectorButton.disabled = el.renderRasterButton.disabled = el.renderBackend.disabled || state.editing;
|
||||
el.renderVectorButton.setAttribute("aria-pressed", String(displayMode() === "svg"));
|
||||
el.renderRasterButton.setAttribute("aria-pressed", String(displayMode() === "raster"));
|
||||
@@ -3152,6 +3155,8 @@ function updateRenderBackend() {
|
||||
el.renderPDF.textContent = renderBackendLabel(backend?.pdf);
|
||||
el.renderEPS.textContent = renderBackendLabel(backend?.eps);
|
||||
el.renderCompiler.textContent = renderBackendLabel(backend?.compiler);
|
||||
el.renderFonts.textContent = renderBackendLabel(backend?.fonts);
|
||||
el.renderGeometry.textContent = renderBackendLabel(backend?.geometry);
|
||||
}
|
||||
|
||||
async function refreshRenderBackend(anchor = scaleAnchor(0)) {
|
||||
@@ -6348,6 +6353,7 @@ function exportFormatUsesDPI(value) {
|
||||
|
||||
function updateDPIControl() {
|
||||
el.imageDPI.disabled = state.exporting || !state.doc || !exportFormatUsesDPI(el.exportFormat.value);
|
||||
el.imageDPI.title = exportFormatUsesDPI(el.exportFormat.value) ? "导出DPI(仅PNG/JPG)" : "当前格式无需DPI";
|
||||
}
|
||||
|
||||
function currentImageDPI() {
|
||||
|
||||
+10
-9
@@ -29,6 +29,7 @@ import (
|
||||
"io"
|
||||
"math"
|
||||
"path"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -374,8 +375,8 @@ func renderPage(args []js.Value) (any, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if currentEditor != nil {
|
||||
currentEditor.SetPageCompiler(currentSession.Renderer.Backends().Compiler)
|
||||
if currentEditor != nil && !reflect.DeepEqual(currentEditor.Backends(), currentSession.Renderer.Backends()) {
|
||||
currentEditor.SetRenderBackends(currentSession.Renderer.Backends())
|
||||
}
|
||||
text, err := currentSession.PageText(args[0].Int())
|
||||
if err != nil {
|
||||
@@ -844,7 +845,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
|
||||
if kind != "" && capability.Update {
|
||||
item["shape"] = string(kind)
|
||||
item["geometry"] = map[string]any{"x": geometry.X, "y": geometry.Y, "width": geometry.W, "height": geometry.H}
|
||||
} else if outline, err := path.Outline(); err == nil {
|
||||
} else if outline, err := currentSession.Renderer.PathOutline(path); err == nil {
|
||||
item["outline"] = outline
|
||||
}
|
||||
if kind == "" && capability.Update {
|
||||
@@ -1162,7 +1163,7 @@ func writeAnnotation(args []js.Value) (any, error) {
|
||||
}
|
||||
if options.Kind == "watermark" {
|
||||
if watermarkBounds.W == 0 {
|
||||
watermarkBounds, err = measureWatermark(annotation.Appearance.Objects[0], fontData)
|
||||
watermarkBounds, err = measureWatermark(annotation.Appearance.Objects[0], fontData, currentEditor.Backends())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1195,9 +1196,9 @@ func writeAnnotation(args []js.Value) (any, error) {
|
||||
}
|
||||
|
||||
// measureWatermark 复用标准渲染度量水印,临时文档保留尚未提交的字体引用
|
||||
// 入参: object 水印图元, fontData 字体字节
|
||||
// 入参: object 水印图元, fontData 字体字节, backends 当前后端配置
|
||||
// 返回: ofdgo.Box 实际绘制范围, error 错误信息
|
||||
func measureWatermark(object ofdgo.GraphicObject, fontData []byte) (ofdgo.Box, error) {
|
||||
func measureWatermark(object ofdgo.GraphicObject, fontData []byte, backends ofdgo.RenderBackends) (ofdgo.Box, error) {
|
||||
if object.Type == "ImageObject" {
|
||||
return ofdgo.NewMatrix(object.ImageObject.CTM).TransformBox(ofdgo.Box{W: 1, H: 1}), nil
|
||||
}
|
||||
@@ -1221,7 +1222,7 @@ func measureWatermark(object ofdgo.GraphicObject, fontData []byte) (ofdgo.Box, e
|
||||
if _, err := reader.PageContentByIndex(0); err != nil {
|
||||
return ofdgo.Box{}, err
|
||||
}
|
||||
bounds, err := ofdgo.NewRenderer(reader).ObjectBounds(ofdgo.GraphicObject{Type: "CompositeObject", CompositeGraphicUnit: ofdgo.CompositeGraphicUnit{
|
||||
bounds, err := ofdgo.NewRenderer(reader, ofdgo.WithRenderBackends(backends)).ObjectBounds(ofdgo.GraphicObject{Type: "CompositeObject", CompositeGraphicUnit: ofdgo.CompositeGraphicUnit{
|
||||
Boundary: object.TextObject.Boundary, Objects: []ofdgo.GraphicObject{object},
|
||||
}}, "")
|
||||
if err != nil {
|
||||
@@ -2781,7 +2782,7 @@ func editDocument(args []js.Value) (any, error) {
|
||||
if currentSession.fontFS != nil {
|
||||
editor.SetFontFS(currentSession.fontFS)
|
||||
}
|
||||
editor.SetPageCompiler(currentSession.Renderer.Backends().Compiler)
|
||||
editor.SetRenderBackends(currentSession.Renderer.Backends())
|
||||
currentEditor = editor
|
||||
currentSession.editing = true
|
||||
copiedObjects = nil
|
||||
@@ -2811,7 +2812,7 @@ func previewEditor(editor *ofdgo.Editor, annotations bool) (editorInfo, error) {
|
||||
}
|
||||
_ = currentSession.Close()
|
||||
}
|
||||
editor.SetPageCompiler(session.Renderer.Backends().Compiler)
|
||||
editor.SetRenderBackends(session.Renderer.Backends())
|
||||
currentSession = session
|
||||
session.editing = true
|
||||
if currentEditor != editor {
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -536,7 +537,11 @@ func (s *Session) SetRenderBackend(name string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.Renderer.Backends().Info().Compiler != backends.Info().Compiler {
|
||||
if reflect.DeepEqual(s.Renderer.Backends(), backends) {
|
||||
return nil
|
||||
}
|
||||
previous, next := s.Renderer.Backends().Info(), backends.Info()
|
||||
if previous.Compiler != next.Compiler || previous.Fonts != next.Fonts || previous.Geometry != next.Geometry {
|
||||
clear(s.textCache)
|
||||
}
|
||||
ofdgo.WithRenderBackends(backends)(s.Renderer)
|
||||
|
||||
@@ -79,7 +79,6 @@ func NewRenderer(reader *Reader, opts ...RendererOption) *Renderer {
|
||||
for _, opt := range opts {
|
||||
opt(r)
|
||||
}
|
||||
r.initCommon()
|
||||
return r
|
||||
}
|
||||
|
||||
|
||||
+40
-85
@@ -18,8 +18,6 @@ import (
|
||||
"encoding/xml"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
// Action 动作
|
||||
@@ -346,7 +344,11 @@ func (r *Renderer) appendGraphicActionSources(sources []actionSource, object Gra
|
||||
source.Box.W, source.Box.H = box.W, box.H
|
||||
}
|
||||
if box.W > 0 && box.H > 0 && !axisAlignedMatrix(placement) {
|
||||
source.Path = canvas.Rectangle(box.W, box.H).Transform(actionCanvasMatrix(placement)).ToSVG()
|
||||
path := geometryRectangle(Box{W: box.W, H: box.H})
|
||||
for i := range path {
|
||||
path[i].End.X, path[i].End.Y = placement.Transform(path[i].End.X, path[i].End.Y)
|
||||
}
|
||||
source.Path, _ = path.SVG()
|
||||
}
|
||||
sources = append(sources, source)
|
||||
}
|
||||
@@ -366,93 +368,27 @@ func (r *Renderer) appendGraphicActionSources(sources []actionSource, object Gra
|
||||
return sources
|
||||
}
|
||||
|
||||
// actionCanvasMatrix 转换页面坐标矩阵,不翻转Y轴
|
||||
// 入参: matrix 页面变换
|
||||
// 返回: canvas.Matrix 路径变换
|
||||
func actionCanvasMatrix(matrix Matrix) canvas.Matrix {
|
||||
return canvas.Matrix{{matrix.a, matrix.c, matrix.e}, {matrix.b, matrix.d, matrix.f}}
|
||||
}
|
||||
|
||||
// actionRegionPath 解析复杂点击区域并转换为页面坐标
|
||||
// 入参: region 动作区域, matrix 坐标变换
|
||||
// 返回: *canvas.Path 点击路径
|
||||
func actionRegionPath(region *Region, matrix Matrix) *canvas.Path {
|
||||
path := &canvas.Path{}
|
||||
for _, area := range region.Area {
|
||||
start := parseFloats(area.Start)
|
||||
if len(start) != 2 {
|
||||
continue
|
||||
}
|
||||
part := &canvas.Path{}
|
||||
part.MoveTo(start[0], start[1])
|
||||
for _, command := range area.Command {
|
||||
p1, p2, p3 := parseFloats(command.Point1), parseFloats(command.Point2), parseFloats(command.Point3)
|
||||
switch command.Type {
|
||||
case "Move":
|
||||
if len(p1) == 2 {
|
||||
part.Close()
|
||||
part.MoveTo(p1[0], p1[1])
|
||||
}
|
||||
case "Line":
|
||||
if len(p1) == 2 {
|
||||
part.LineTo(p1[0], p1[1])
|
||||
}
|
||||
case "QuadraticBezier":
|
||||
if len(p1) == 2 && len(p2) == 2 {
|
||||
part.QuadTo(p1[0], p1[1], p2[0], p2[1])
|
||||
}
|
||||
case "CubicBezier":
|
||||
if len(p3) == 2 {
|
||||
first, second := part.Pos(), canvas.Point{X: p3[0], Y: p3[1]}
|
||||
if len(p1) == 2 {
|
||||
first = canvas.Point{X: p1[0], Y: p1[1]}
|
||||
}
|
||||
if len(p2) == 2 {
|
||||
second = canvas.Point{X: p2[0], Y: p2[1]}
|
||||
}
|
||||
part.CubeTo(first.X, first.Y, second.X, second.Y, p3[0], p3[1])
|
||||
}
|
||||
case "Arc":
|
||||
size, end := parseFloats(command.EllipseSize), parseFloats(command.EndPoint)
|
||||
angle, err := strconv.ParseFloat(command.RotationAngle, 64)
|
||||
large, largeErr := strconv.ParseBool(command.LargeArc)
|
||||
sweep, sweepErr := strconv.ParseBool(command.SweepDirection)
|
||||
if len(end) == 2 && err == nil && largeErr == nil && sweepErr == nil {
|
||||
rx, ry := 0.0, 0.0
|
||||
if len(size) > 0 {
|
||||
rx, ry = size[0], size[0]
|
||||
}
|
||||
if len(size) > 1 {
|
||||
ry = size[1]
|
||||
}
|
||||
part.ArcTo(rx, ry, angle, large, sweep, end[0], end[1])
|
||||
}
|
||||
case "Close":
|
||||
part.Close()
|
||||
}
|
||||
}
|
||||
part.Close()
|
||||
path = path.Append(part)
|
||||
}
|
||||
if canvas.Equal(matrix.a*matrix.d-matrix.b*matrix.c, 0) {
|
||||
path = path.ReplaceArcs()
|
||||
}
|
||||
return path.Transform(actionCanvasMatrix(matrix))
|
||||
}
|
||||
|
||||
// actionLinkRegion 获取动作点击区域
|
||||
// 入参: source 动作来源, action 动作
|
||||
// 返回: Box 外接矩形, string 精确路径
|
||||
func actionLinkRegion(source actionSource, action Action) (Box, string) {
|
||||
// 返回: Box 外接矩形, string 精确路径, error 几何错误
|
||||
func (r *Renderer) actionLinkRegion(source actionSource, action Action) (Box, string, error) {
|
||||
if action.Region == nil {
|
||||
return source.Box, source.Path
|
||||
return source.Box, source.Path, nil
|
||||
}
|
||||
path := actionRegionPath(action.Region, source.Matrix)
|
||||
if path.Empty() {
|
||||
return Box{}, ""
|
||||
geometry, err := r.Geometry()
|
||||
if err != nil {
|
||||
return Box{}, "", err
|
||||
}
|
||||
box := path.Bounds()
|
||||
return Box{X: box.X0, Y: box.Y0, W: box.W(), H: box.H()}, path.ToSVG()
|
||||
path, err := geometry.Region(action.Region, source.Matrix)
|
||||
if err != nil {
|
||||
return Box{}, "", err
|
||||
}
|
||||
box, err := geometry.Bounds(path)
|
||||
if err != nil {
|
||||
return Box{}, "", err
|
||||
}
|
||||
outline, err := path.SVG()
|
||||
return box, outline, err
|
||||
}
|
||||
|
||||
// resolveActionURI 解析URI动作地址
|
||||
@@ -472,3 +408,22 @@ func resolveActionURI(action URI) string {
|
||||
}
|
||||
return base.ResolveReference(target).String()
|
||||
}
|
||||
|
||||
// outlineDest 获取大纲跳转目标
|
||||
// 入参: outline 大纲节点, bookmarks 书签
|
||||
// 返回: *Dest 跳转目标
|
||||
func outlineDest(outline OutlineElem, bookmarks map[string]Dest) *Dest {
|
||||
for _, action := range outline.Actions {
|
||||
if action.Goto != nil {
|
||||
if dest := gotoDest(action.Goto, bookmarks); dest != nil {
|
||||
return dest
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, child := range outline.OutlineElem {
|
||||
if dest := outlineDest(child, bookmarks); dest != nil {
|
||||
return dest
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+10
-3
@@ -104,6 +104,8 @@ type EPSBackend interface {
|
||||
// RenderBackends 按职责组合后端,nil表示明确禁用,不隐式回退
|
||||
// 实例及其Renderer需串行使用,第三方实现无需引用任何内置绘图库
|
||||
type RenderBackends struct {
|
||||
Fonts FontBackend
|
||||
Geometry GeometryBackend
|
||||
Compiler PageCompiler
|
||||
Raster RasterBackend
|
||||
SVG SVGBackend
|
||||
@@ -113,6 +115,8 @@ type RenderBackends struct {
|
||||
|
||||
// BackendInfo 描述各项能力实际使用的后端,空标识表示未配置
|
||||
type BackendInfo struct {
|
||||
Fonts string `json:"fonts"`
|
||||
Geometry string `json:"geometry"`
|
||||
Compiler string `json:"compiler"`
|
||||
Raster string `json:"raster"`
|
||||
SVG string `json:"svg"`
|
||||
@@ -150,13 +154,13 @@ func RenderBackendInfos() map[string]BackendInfo {
|
||||
// 返回: RenderBackends 默认后端组合
|
||||
func defaultRenderBackends() RenderBackends {
|
||||
backend := CanvasBackend{}
|
||||
return RenderBackends{Compiler: backend, Raster: backend, SVG: backend, PDF: backend, EPS: backend}
|
||||
return RenderBackends{Fonts: backend, Geometry: backend, Compiler: backend, Raster: backend, SVG: backend, PDF: backend, EPS: backend}
|
||||
}
|
||||
|
||||
// Info 返回配置中各能力的提供者,不依赖WebUI推断
|
||||
// 返回: BackendInfo 后端能力
|
||||
func (b RenderBackends) Info() BackendInfo {
|
||||
return BackendInfo{Compiler: backendName(b.Compiler), Raster: backendName(b.Raster), SVG: backendName(b.SVG), PDF: backendName(b.PDF), EPS: backendName(b.EPS)}
|
||||
return BackendInfo{Fonts: backendName(b.Fonts), Geometry: backendName(b.Geometry), Compiler: backendName(b.Compiler), Raster: backendName(b.Raster), SVG: backendName(b.SVG), PDF: backendName(b.PDF), EPS: backendName(b.EPS)}
|
||||
}
|
||||
|
||||
// backendName 获取已配置的后端标识
|
||||
@@ -173,7 +177,10 @@ func backendName(backend Backend) string {
|
||||
// 入参: backends 后端组合
|
||||
// 返回: RendererOption 渲染选项
|
||||
func WithRenderBackends(backends RenderBackends) RendererOption {
|
||||
return func(r *Renderer) { r.backends = backends }
|
||||
return func(r *Renderer) {
|
||||
r.backends = backends
|
||||
r.resetFontCache()
|
||||
}
|
||||
}
|
||||
|
||||
// Backends 返回当前后端配置副本,可修改后通过WithRenderBackends应用
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
// 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
|
||||
|
||||
const defaultPathLineWidth = 0.353
|
||||
const defaultMiterLimit = 3.528
|
||||
|
||||
// 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, defaults 默认绘制参数
|
||||
// 返回: *DrawParam 合并后的绘制参数
|
||||
func (r *Renderer) drawParamDefaults(id string, defaults *DrawParam) *DrawParam {
|
||||
if id == "" {
|
||||
return defaults
|
||||
}
|
||||
dp := r.getDrawParam(id, nil)
|
||||
if dp == nil {
|
||||
return defaults
|
||||
}
|
||||
if defaults == nil {
|
||||
return dp
|
||||
}
|
||||
return mergeDrawParam(*defaults, dp)
|
||||
}
|
||||
|
||||
// mergeDrawParam 合并绘制参数属性
|
||||
// 入参: base 基础绘制参数, dp 覆盖绘制参数
|
||||
// 返回: *DrawParam 合并后的绘制参数
|
||||
func mergeDrawParam(base DrawParam, dp *DrawParam) *DrawParam {
|
||||
if dp.LineWidth > 0 {
|
||||
base.LineWidth = dp.LineWidth
|
||||
}
|
||||
if dp.Join != "" {
|
||||
base.Join = dp.Join
|
||||
}
|
||||
if dp.Cap != "" {
|
||||
base.Cap = dp.Cap
|
||||
}
|
||||
if dp.DashPattern != "" || dp.dashPatternSet {
|
||||
base.DashPattern = dp.DashPattern
|
||||
base.dashPatternSet = dp.dashPatternSet
|
||||
}
|
||||
if dp.DashOffset != nil {
|
||||
base.DashOffset = dp.DashOffset
|
||||
}
|
||||
if dp.MiterLimit > 0 {
|
||||
base.MiterLimit = dp.MiterLimit
|
||||
}
|
||||
if dp.FillColor != nil {
|
||||
base.FillColor = dp.FillColor
|
||||
}
|
||||
if dp.StrokeColor != nil {
|
||||
base.StrokeColor = dp.StrokeColor
|
||||
}
|
||||
if dp.Font != "" {
|
||||
base.Font = dp.Font
|
||||
}
|
||||
if dp.Size > 0 {
|
||||
base.Size = dp.Size
|
||||
}
|
||||
if dp.Weight > 0 {
|
||||
base.Weight = dp.Weight
|
||||
}
|
||||
if dp.Italic {
|
||||
base.Italic = dp.Italic
|
||||
}
|
||||
return &base
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
return mergeDrawParam(*base, dp)
|
||||
}
|
||||
return dp
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+28
-96
@@ -48,7 +48,8 @@ type Editor struct {
|
||||
fonts map[string]*font.SFNT
|
||||
fontFS []fs.FS
|
||||
fontRenderer *Renderer
|
||||
compiler PageCompiler
|
||||
fontMetrics map[string]FontMetrics
|
||||
backends RenderBackends
|
||||
images map[string]image.Point
|
||||
resourceID map[editorResourceKey]string
|
||||
maxID int
|
||||
@@ -67,25 +68,38 @@ type Editor struct {
|
||||
func (e *Editor) SetFontFS(fsys ...fs.FS) {
|
||||
e.fontFS = slices.Clone(fsys)
|
||||
e.fontRenderer = nil
|
||||
e.fontMetrics = make(map[string]FontMetrics)
|
||||
}
|
||||
|
||||
// SetPageCompiler 设置编辑对象度量使用的页面编译器,不改变文档或撤销历史
|
||||
// 与Renderer使用相同实例可统一显示、搜索和选区,nil禁用度量,不隐式回退
|
||||
// 入参: compiler 页面编译器
|
||||
func (e *Editor) SetPageCompiler(compiler PageCompiler) {
|
||||
e.compiler = compiler
|
||||
e.backends.Compiler = compiler
|
||||
if e.fontRenderer != nil {
|
||||
e.fontRenderer.backends.Compiler = compiler
|
||||
}
|
||||
}
|
||||
|
||||
// newRenderer 使用编辑器字体来源和页面编译器创建资源度量器
|
||||
// SetRenderBackends 设置完整后端组合并重置字体度量,不改变文档或撤销历史
|
||||
// 入参: backends 后端组合,未配置的能力不会沿用默认实现
|
||||
func (e *Editor) SetRenderBackends(backends RenderBackends) {
|
||||
e.backends = backends
|
||||
e.fontRenderer = nil
|
||||
e.fontMetrics = make(map[string]FontMetrics)
|
||||
}
|
||||
|
||||
// Backends 返回编辑器后端配置副本
|
||||
// 返回: RenderBackends 后端组合
|
||||
func (e *Editor) Backends() RenderBackends {
|
||||
return e.backends
|
||||
}
|
||||
|
||||
// newRenderer 使用编辑器字体来源和完整后端配置创建资源度量器
|
||||
// 入参: reader 当前文档快照
|
||||
// 返回: *Renderer 度量器
|
||||
func (e *Editor) newRenderer(reader *Reader) *Renderer {
|
||||
backends := defaultRenderBackends()
|
||||
backends.Compiler = e.compiler
|
||||
return NewRenderer(reader, WithFontFS(e.fontFS...), WithRenderBackends(backends))
|
||||
return NewRenderer(reader, WithFontFS(e.fontFS...), WithRenderBackends(e.backends))
|
||||
}
|
||||
|
||||
// editorResource 文档内嵌资源
|
||||
@@ -133,7 +147,8 @@ func NewEditor() *Editor {
|
||||
fonts: make(map[string]*font.SFNT),
|
||||
images: make(map[string]image.Point),
|
||||
resourceID: make(map[editorResourceKey]string),
|
||||
compiler: CanvasBackend{},
|
||||
fontMetrics: make(map[string]FontMetrics),
|
||||
backends: defaultRenderBackends(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1064,102 +1079,19 @@ func scaleTextNumbers(value string, scale float64) string {
|
||||
return strings.Join(fields, " ")
|
||||
}
|
||||
|
||||
// LayoutText 按嵌入字体度量和段落选项重排横向文字,保留绘制属性,不进行复杂文字塑形
|
||||
// 不修改文档,通过AddObject或UpdateObject提交;选项仅供当前编辑过程使用,保存为标准文字定位
|
||||
// LayoutText 使用编辑器字体后端重排文字,不修改文档和历史
|
||||
// 入参: obj 文字对象, value 原文, options 段落排版选项
|
||||
// 返回: error 错误信息
|
||||
// 返回: error 字体或排版错误
|
||||
func (e *Editor) LayoutText(obj *TextObject, value string, options TextLayout) error {
|
||||
if obj.ReadDirection != 0 || obj.CharDirection != 0 {
|
||||
return fmt.Errorf("automatic text layout requires horizontal text")
|
||||
}
|
||||
sfnt, err := e.editorFont(obj.Font)
|
||||
metrics, err := e.editorFont(obj.Font)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !finite(obj.Size) || obj.Size <= 0 || !finite(obj.HScale) || obj.HScale < 0 {
|
||||
return fmt.Errorf("invalid text dimensions")
|
||||
}
|
||||
value = strings.ReplaceAll(strings.ReplaceAll(value, "\r\n", "\n"), "\r", "\n")
|
||||
if !utf8.ValidString(value) || strings.Contains(value, "\t") || strings.Trim(value, "\n") == "" {
|
||||
return fmt.Errorf("text must contain UTF-8 characters without tabs")
|
||||
}
|
||||
lineHeight := options.LineHeight
|
||||
if !finite(lineHeight) || lineHeight < 0 {
|
||||
return fmt.Errorf("line height must be finite and nonnegative")
|
||||
}
|
||||
if !finite(options.LetterSpacing) {
|
||||
return fmt.Errorf("letter spacing must be finite")
|
||||
}
|
||||
if !finite(options.LeftIndent) || !finite(options.RightIndent) || !finite(options.FirstLineIndent) {
|
||||
return fmt.Errorf("paragraph indents must be finite")
|
||||
}
|
||||
if !slices.Contains([]string{"", "left", "center", "right", "justify"}, options.Align) {
|
||||
return fmt.Errorf("invalid text alignment %q", options.Align)
|
||||
}
|
||||
var width float64
|
||||
if options.Wrap || options.Align != "" && options.Align != "left" || options.LeftIndent != 0 || options.RightIndent != 0 || options.FirstLineIndent != 0 {
|
||||
box, err := obj.TextFrame()
|
||||
if err != nil {
|
||||
if err := LayoutText(obj, value, options, metrics); err != nil {
|
||||
return err
|
||||
}
|
||||
width = box.W - options.LeftIndent - options.RightIndent
|
||||
if !finite(width) || width <= 0 || !finite(width-options.FirstLineIndent) || width-options.FirstLineIndent <= 0 {
|
||||
return fmt.Errorf("paragraph indents leave no text width")
|
||||
}
|
||||
}
|
||||
hScale := obj.HScale
|
||||
if hScale == 0 {
|
||||
hScale = 1
|
||||
}
|
||||
ascender, descender, gap := sfnt.VerticalMetrics()
|
||||
unit := obj.Size / float64(sfnt.UnitsPerEm())
|
||||
if lineHeight == 0 {
|
||||
lineHeight = math.Max(obj.Size, float64(int(ascender)-int(descender)+int(gap))*unit)
|
||||
}
|
||||
var codes []TextCode
|
||||
var missing []rune
|
||||
for _, paragraph := range strings.Split(value, "\n") {
|
||||
runes := []rune(paragraph)
|
||||
advances := make([]float64, len(runes))
|
||||
for i, char := range runes {
|
||||
glyph := sfnt.GlyphIndex(char)
|
||||
if glyph == 0 {
|
||||
missing = append(missing, char)
|
||||
continue
|
||||
}
|
||||
advances[i] = float64(sfnt.GlyphAdvance(glyph)) * unit * hScale
|
||||
}
|
||||
if len(missing) != 0 {
|
||||
continue
|
||||
}
|
||||
spaceTextAdvances(runes, advances, options.LetterSpacing)
|
||||
for _, advance := range advances {
|
||||
if !finite(advance) {
|
||||
return fmt.Errorf("text advance exceeds finite range")
|
||||
}
|
||||
}
|
||||
lines := breakTextLines(runes, advances, width, options.Wrap, options.LetterSpacing, options.FirstLineIndent)
|
||||
for i, line := range lines {
|
||||
indent := 0.0
|
||||
if i == 0 {
|
||||
indent = options.FirstLineIndent
|
||||
}
|
||||
x, deltas := alignTextLine(runes[line[0]:line[1]], advances[line[0]:line[1]], width-indent, options.Align, i+1 < len(lines), options.LetterSpacing)
|
||||
x += options.LeftIndent + indent
|
||||
y := float64(ascender)*unit + float64(len(codes))*lineHeight
|
||||
if !finite(x) || !finite(y) {
|
||||
return fmt.Errorf("text position exceeds finite range")
|
||||
}
|
||||
codes = append(codes, TextCode{X: ofdNumber(x), Y: ofdNumber(y), DeltaX: deltas, Value: escapeOFDText(string(runes[line[0]:line[1]]))})
|
||||
}
|
||||
}
|
||||
if err := missingGlyphError(obj.Font, missing); err != nil {
|
||||
return err
|
||||
}
|
||||
obj.TextCode = codes
|
||||
obj.layout = nil
|
||||
if options != (TextLayout{}) || e.objectOrigin(obj.ID) != nil {
|
||||
obj.layout = &textLayout{value: value, options: options}
|
||||
if obj.layout == nil && e.objectOrigin(obj.ID) != nil {
|
||||
obj.layout = &textLayout{value: obj.Text(), options: options}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -23,8 +23,6 @@ import (
|
||||
"math"
|
||||
"reflect"
|
||||
"slices"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
// ObjectPath 定位页面顶层对象或注解外观内的成员,Children为从0开始的逐层绘制序号
|
||||
@@ -60,7 +58,7 @@ type editorCompositeNode struct {
|
||||
boundaryInCTM bool
|
||||
defaults *DrawParam
|
||||
drawParams []string
|
||||
clip *canvas.Path
|
||||
clip *GeometryPath
|
||||
visible bool
|
||||
alpha *int
|
||||
ref *editorCompositeNode
|
||||
@@ -404,7 +402,17 @@ func (e *Editor) compositeMembers(n *editorCompositeNode, reader *Reader, render
|
||||
copy.TransFlag = nil
|
||||
clips, clipMatrix = ©, boundary
|
||||
}
|
||||
clip := intersectClipPath(n.clip, renderer.buildClipPath(clips, 0, 0, 0, clipMatrix))
|
||||
clip := n.clip
|
||||
if clips != nil {
|
||||
geometry, err := renderer.Geometry()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clip, err = geometry.Clip(renderer, clips, clipMatrix, n.clip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
defaults := renderer.drawParamDefaults(c.DrawParam, n.defaults)
|
||||
drawParams := append(slices.Clone(n.drawParams), c.DrawParam)
|
||||
visible := n.visible && (c.Visible == nil || *c.Visible)
|
||||
@@ -529,7 +537,7 @@ func (e *Editor) measureCompositeMembers(renderer *Renderer, nodes []*editorComp
|
||||
}
|
||||
if node.visible {
|
||||
var err error
|
||||
bounds, err = renderer.MeasureObject(object, MeasureOptions{Defaults: node.defaults, Parent: &node.parent, BoundaryInCTM: node.boundaryInCTM, Clip: geometryCanvasPath(node.clip), Contours: true})
|
||||
bounds, err = renderer.MeasureObject(object, MeasureOptions{Defaults: node.defaults, Parent: &node.parent, BoundaryInCTM: node.boundaryInCTM, Clip: node.clip, Contours: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -19,9 +19,6 @@ import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
// CompositeSelection 同一内部容器的独立选区快照,共享不可变字体和图片资源
|
||||
@@ -107,7 +104,7 @@ func (e *Editor) pasteCompositeSelection(selection *CompositeSelection, paste fu
|
||||
object = style
|
||||
}
|
||||
object = mergeGraphicObjectAlpha(object, source.alpha)
|
||||
if !source.visible || source.clip != nil && source.clip.Empty() {
|
||||
if !source.visible || source.clip != nil && len(*source.clip) == 0 {
|
||||
visible := false
|
||||
switch object.Type {
|
||||
case "TextObject":
|
||||
@@ -135,8 +132,11 @@ func (e *Editor) pasteCompositeSelection(selection *CompositeSelection, paste fu
|
||||
if err := e.isolateCompositeStyle(copy); err != nil {
|
||||
return err
|
||||
}
|
||||
if source.clip != nil && !source.clip.Empty() {
|
||||
shape := compositeClipPath(source.clip)
|
||||
if source.clip != nil && len(*source.clip) != 0 {
|
||||
shape, err := geometryClipPath(*source.clip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clips := *editorObjectClips(©.object)
|
||||
matrix, ok := copy.matrix(clips == nil || clips.TransFlag == nil || *clips.TransFlag).Invert()
|
||||
if !ok {
|
||||
@@ -224,44 +224,6 @@ func (n *editorCompositeNode) convertCoordinates(parent Matrix, inward bool) err
|
||||
return nil
|
||||
}
|
||||
|
||||
// compositeClipPath 将已解析的父裁剪转换为标准紧缩路径,使用页面毫米坐标
|
||||
// 入参: path 画布裁剪
|
||||
// 返回: PathObject 标准裁剪路径
|
||||
func compositeClipPath(path *canvas.Path) PathObject {
|
||||
return editorClipPath(path.Copy().Transform(canvas.Matrix{{1, 0, 0}, {0, -1, 0}}))
|
||||
}
|
||||
|
||||
// editorClipPath 将页面坐标路径写为标准紧缩裁剪路径
|
||||
// 入参: path 页面毫米路径
|
||||
// 返回: PathObject 标准裁剪路径
|
||||
func editorClipPath(path *canvas.Path) PathObject {
|
||||
path = path.ReplaceArcs()
|
||||
data := path.Data()
|
||||
var value strings.Builder
|
||||
for i := 0; i < len(data); {
|
||||
switch data[i] {
|
||||
case canvas.MoveToCmd, canvas.LineToCmd:
|
||||
command := "L"
|
||||
if data[i] == canvas.MoveToCmd {
|
||||
command = "M"
|
||||
}
|
||||
fmt.Fprintf(&value, "%s %s %s ", command, ofdNumber(data[i+1]), ofdNumber(data[i+2]))
|
||||
i += 4
|
||||
case canvas.QuadToCmd:
|
||||
fmt.Fprintf(&value, "Q %s %s %s %s ", ofdNumber(data[i+1]), ofdNumber(data[i+2]), ofdNumber(data[i+3]), ofdNumber(data[i+4]))
|
||||
i += 6
|
||||
case canvas.CubeToCmd:
|
||||
fmt.Fprintf(&value, "B %s %s %s %s %s %s ", ofdNumber(data[i+1]), ofdNumber(data[i+2]), ofdNumber(data[i+3]), ofdNumber(data[i+4]), ofdNumber(data[i+5]), ofdNumber(data[i+6]))
|
||||
i += 8
|
||||
case canvas.CloseCmd:
|
||||
value.WriteString("C ")
|
||||
i += 4
|
||||
}
|
||||
}
|
||||
fill, stroke := true, false
|
||||
return PathObject{Boundary: "0 0 1 1", Fill: &fill, Stroke: &stroke, AbbreviatedData: strings.TrimSpace(value.String())}
|
||||
}
|
||||
|
||||
// CaptureCompositeObjects 捕获内部选区,不修改文档或分配资源
|
||||
// 入参: page 页面索引, path 父复合路径, indexes 同一容器的成员序号
|
||||
// 返回: *CompositeSelection 独立快照, error 错误信息
|
||||
|
||||
+63
-27
@@ -20,8 +20,6 @@ import (
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
// EraseObjects 擦除同页对象与矩形区域重叠的可见部分,一次撤销恢复全部
|
||||
@@ -41,34 +39,41 @@ func (e *Editor) EraseObjects(page int, ids []string, box Box) error {
|
||||
// 入参: page 页面索引, ids 对象标识, points 页面毫米坐标,至少三个点
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) EraseObjectsPath(page int, ids []string, points []Point) error {
|
||||
region, err := editorEraseRegion(points)
|
||||
if err != nil || region.Empty() {
|
||||
region, err := e.editorEraseRegion(points)
|
||||
if err != nil || len(region) == 0 {
|
||||
return err
|
||||
}
|
||||
bounds := region.FastBounds()
|
||||
return e.eraseObjects(page, ids, Box{X: bounds.X0, Y: bounds.Y0, W: bounds.W(), H: bounds.H()}, points, region)
|
||||
bounds, err := e.backends.Geometry.Bounds(region)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.eraseObjects(page, ids, bounds, points, region)
|
||||
}
|
||||
|
||||
// editorEraseRegion 校验闭合折线并按奇偶规则构建擦除区域
|
||||
// 入参: points 页面毫米坐标
|
||||
// 返回: *canvas.Path 擦除区域, error 错误信息
|
||||
func editorEraseRegion(points []Point) (*canvas.Path, error) {
|
||||
// 返回: GeometryPath 擦除区域, error 错误信息
|
||||
func (e *Editor) editorEraseRegion(points []Point) (GeometryPath, error) {
|
||||
geometry, err := e.Geometry()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(points) < 3 {
|
||||
return nil, fmt.Errorf("erase path requires at least three points")
|
||||
}
|
||||
region := &canvas.Path{}
|
||||
region := make(GeometryPath, 0, len(points)+1)
|
||||
for i, point := range points {
|
||||
if !finite(point.X) || !finite(point.Y) {
|
||||
return nil, fmt.Errorf("invalid erase path point %d", i)
|
||||
}
|
||||
verb := GeometryLine
|
||||
if i == 0 {
|
||||
region.MoveTo(point.X, point.Y)
|
||||
} else {
|
||||
region.LineTo(point.X, point.Y)
|
||||
verb = GeometryMove
|
||||
}
|
||||
region = append(region, GeometrySegment{Verb: verb, End: point})
|
||||
}
|
||||
region.Close()
|
||||
return region.Settle(canvas.EvenOdd), nil
|
||||
region = append(region, GeometrySegment{Verb: GeometryClose, End: points[0]})
|
||||
return geometry.Normalize(region, true)
|
||||
}
|
||||
|
||||
// EraseCompositeObjects 擦除内部成员与矩形相交的部分,保留原始属性及共享资源
|
||||
@@ -79,7 +84,7 @@ func (e *Editor) EraseCompositeObjects(page int, path ObjectPath, indexes []int,
|
||||
if _, err := creationBox(editorBoxString(box)); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.eraseCompositeObjects(page, path, indexes, canvas.Rectangle(box.W, box.H).Translate(box.X, box.Y))
|
||||
return e.eraseCompositeObjects(page, path, indexes, geometryRectangle(box))
|
||||
}
|
||||
|
||||
// EraseCompositeObjectsPath 按闭合折线擦除内部成员,末点自动连接起点
|
||||
@@ -87,8 +92,8 @@ func (e *Editor) EraseCompositeObjects(page int, path ObjectPath, indexes []int,
|
||||
// 入参: page 页面索引, path 父路径, indexes 成员序号, points 页面毫米坐标
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) EraseCompositeObjectsPath(page int, path ObjectPath, indexes []int, points []Point) error {
|
||||
region, err := editorEraseRegion(points)
|
||||
if err != nil || region.Empty() {
|
||||
region, err := e.editorEraseRegion(points)
|
||||
if err != nil || len(region) == 0 {
|
||||
return err
|
||||
}
|
||||
return e.eraseCompositeObjects(page, path, indexes, region)
|
||||
@@ -97,15 +102,30 @@ func (e *Editor) EraseCompositeObjectsPath(page int, path ObjectPath, indexes []
|
||||
// eraseCompositeObjects 按页面区域裁剪内部成员,全部成功后提交一次历史
|
||||
// 入参: page 页面索引, path 父路径, indexes 成员序号, region 擦除区域
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) eraseCompositeObjects(page int, path ObjectPath, indexes []int, region *canvas.Path) error {
|
||||
func (e *Editor) eraseCompositeObjects(page int, path ObjectPath, indexes []int, region GeometryPath) error {
|
||||
geometry, err := e.Geometry()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||
for i, node := range nodes {
|
||||
box := members[i].Bounds
|
||||
bounds := canvas.Rectangle(box.W, box.H).Translate(box.X, box.Y)
|
||||
if box.W <= 0 || box.H <= 0 || bounds.And(region).Empty() {
|
||||
if box.W <= 0 || box.H <= 0 {
|
||||
continue
|
||||
}
|
||||
if bounds.Not(region).Empty() {
|
||||
bounds := geometryRectangle(box)
|
||||
intersection, err := geometry.Combine(bounds, region, GeometryIntersect)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(intersection) == 0 {
|
||||
continue
|
||||
}
|
||||
remainder, err := geometry.Combine(bounds, region, GeometrySubtract)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(remainder) == 0 {
|
||||
node.owner.patches = append(node.owner.patches, editorXMLPatch{node.span.start, node.span.end, nil})
|
||||
continue
|
||||
}
|
||||
@@ -117,8 +137,16 @@ func (e *Editor) eraseCompositeObjects(page int, path ObjectPath, indexes []int,
|
||||
if !ok {
|
||||
return fmt.Errorf("composite clip transform is not invertible")
|
||||
}
|
||||
outer := canvas.Rectangle(box.W+2, box.H+2).Translate(box.X-1, box.Y-1)
|
||||
if err := appendCompositeClip(node, editorClipPath(outer.Not(region)), inverse, false); err != nil {
|
||||
outer := geometryRectangle(Box{X: box.X - 1, Y: box.Y - 1, W: box.W + 2, H: box.H + 2})
|
||||
remainder, err = geometry.Combine(outer, region, GeometrySubtract)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shape, err := geometryClipPath(remainder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appendCompositeClip(node, shape, inverse, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -129,7 +157,7 @@ func (e *Editor) eraseCompositeObjects(page int, path ObjectPath, indexes []int,
|
||||
// eraseObjects 按矩形或闭合路径裁剪对象,只在全部对象处理成功后提交修改
|
||||
// 入参: page 页面索引, ids 对象标识, box 区域边界, points 折线点, region 奇偶填充后的路径
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) eraseObjects(page int, ids []string, box Box, points []Point, region *canvas.Path) error {
|
||||
func (e *Editor) eraseObjects(page int, ids []string, box Box, points []Point, region GeometryPath) error {
|
||||
objects, indexes, err := e.selectedObjects(page, ids)
|
||||
if err != nil || len(objects) == 0 {
|
||||
return err
|
||||
@@ -150,11 +178,19 @@ func (e *Editor) eraseObjects(page int, ids []string, box Box, points []Point, r
|
||||
}
|
||||
covered := x == bounds.X && y == bounds.Y && right == bounds.X+bounds.W && bottom == bounds.Y+bounds.H
|
||||
if region != nil {
|
||||
path := canvas.Rectangle(bounds.W, bounds.H).Translate(bounds.X, bounds.Y)
|
||||
if path.And(region).Empty() {
|
||||
path := geometryRectangle(bounds)
|
||||
intersection, err := e.backends.Geometry.Combine(path, region, GeometryIntersect)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(intersection) == 0 {
|
||||
continue
|
||||
}
|
||||
covered = path.Not(region).Empty()
|
||||
remainder, err := e.backends.Geometry.Combine(path, region, GeometrySubtract)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
covered = len(remainder) == 0
|
||||
}
|
||||
if covered {
|
||||
removed[ids[i]] = true
|
||||
|
||||
@@ -60,6 +60,7 @@ func (e *Editor) transactionSnapshot() Editor {
|
||||
next.resources = slices.Clone(e.resources)
|
||||
next.fonts, next.images, next.resourceID = maps.Clone(e.fonts), maps.Clone(e.images), maps.Clone(e.resourceID)
|
||||
next.origins = maps.Clone(e.origins)
|
||||
next.fontMetrics = maps.Clone(e.fontMetrics)
|
||||
if e.source != nil {
|
||||
source := *e.source
|
||||
source.pages, source.origins = maps.Clone(source.pages), maps.Clone(source.origins)
|
||||
@@ -80,6 +81,8 @@ func (e *Editor) restoreTransaction(state Editor) {
|
||||
maps.Copy(state.resourceID, e.resourceID)
|
||||
state.history, state.historyIndex, state.historyLimit = e.history, e.historyIndex, e.historyLimit
|
||||
state.serial, state.maxID = max(e.serial, state.serial), max(e.maxID, state.maxID)
|
||||
state.backends, state.fontFS = e.backends, e.fontFS
|
||||
state.fontRenderer, state.fontMetrics = e.fontRenderer, e.fontMetrics
|
||||
*e = state
|
||||
}
|
||||
|
||||
|
||||
+25
-28
@@ -25,9 +25,6 @@ import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
"github.com/tdewolff/font"
|
||||
)
|
||||
|
||||
// editorSource 保留输入包及按需加载的原始页面
|
||||
@@ -629,41 +626,41 @@ func editorGeometry(object GraphicObject) (string, string) {
|
||||
|
||||
// editorFont 按需解析内嵌字体或名称匹配的外部字体,不使用无关回退字体
|
||||
// 入参: id 字体资源标识
|
||||
// 返回: *font.SFNT 字体, error 错误信息
|
||||
func (e *Editor) editorFont(id string) (*font.SFNT, error) {
|
||||
if sfnt := e.fonts[id]; sfnt != nil {
|
||||
return sfnt, nil
|
||||
// 返回: FontMetrics 字体度量, error 错误信息
|
||||
func (e *Editor) editorFont(id string) (FontMetrics, error) {
|
||||
if metrics := e.fontMetrics[id]; metrics != nil {
|
||||
return metrics, nil
|
||||
}
|
||||
if e.source != nil {
|
||||
definition := e.source.reader.fontCache[id]
|
||||
if definition != nil && definition.FontFile == "" {
|
||||
if e.backends.Fonts == nil {
|
||||
return nil, &EditError{Code: EditFontUnavailable, Err: fmt.Errorf("fonts: %w", ErrBackendUnavailable)}
|
||||
}
|
||||
var data []byte
|
||||
for _, resource := range e.resources {
|
||||
if resource.font != nil && resource.font.ID == id {
|
||||
data = resource.data
|
||||
break
|
||||
}
|
||||
}
|
||||
if data == nil && e.source != nil {
|
||||
if e.fontRenderer == nil {
|
||||
e.fontRenderer = e.newRenderer(e.source.reader)
|
||||
}
|
||||
style := canvasFontStyle(definition)
|
||||
for _, source := range e.fontRenderer.fontSources(id, definition, style) {
|
||||
if !source.exact {
|
||||
continue
|
||||
}
|
||||
if family := e.fontRenderer.loadFontSource(canvas.NewFontFamily(definition.FontName), source, style); family != nil {
|
||||
return family.Face(12, style).Font.SFNT, nil
|
||||
}
|
||||
}
|
||||
return nil, &EditError{Code: EditFontUnavailable, Err: fmt.Errorf("font %q is unavailable", definition.FontName)}
|
||||
}
|
||||
data, err := e.source.reader.FontData(id)
|
||||
resolved, err := e.fontRenderer.ResolveFont(id, true)
|
||||
if err != nil {
|
||||
return nil, &EditError{Code: EditFontUnavailable, Err: err}
|
||||
}
|
||||
sfnt, err := font.ParseSFNT(data, 0)
|
||||
if err != nil {
|
||||
return nil, &EditError{Code: EditFontUnavailable, Err: err}
|
||||
}
|
||||
e.fonts[id] = sfnt
|
||||
return sfnt, nil
|
||||
data = resolved.Data
|
||||
}
|
||||
if data == nil {
|
||||
return nil, &EditError{Code: EditFontUnavailable, Err: fmt.Errorf("font %q not found", id)}
|
||||
}
|
||||
metrics, err := e.backends.Fonts.OpenFont(data)
|
||||
if err != nil {
|
||||
return nil, &EditError{Code: EditFontUnavailable, Err: err}
|
||||
}
|
||||
e.fontMetrics[id] = metrics
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// FontData 获取编辑使用的字体数据,外部字体不写入文档资源
|
||||
// 入参: id 字体资源标识
|
||||
|
||||
@@ -16,8 +16,18 @@ package ofdgo
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/tdewolff/font"
|
||||
)
|
||||
|
||||
// fontSFNTData 保留字体原始表数据和时间,重新计算目录与校验和
|
||||
// 入参: sfnt 已解析的字体
|
||||
// 返回: []byte 独立SFNT数据
|
||||
func fontSFNTData(sfnt *font.SFNT) []byte {
|
||||
data, _ := serializeOTF(sfnt.Tables)
|
||||
return data
|
||||
}
|
||||
|
||||
// FixFontDataAggressive 激进修复字体数据
|
||||
// 尝试修复缺失表(OS/2, cmap等)的TrueType字体或包装CFF裸数据
|
||||
// 对显式字形映射补充私有字符cmap,便于按glyph id渲染
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// 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"
|
||||
|
||||
// FontMetrics 提供不依赖绘图库的字体度量,所有整数尺寸使用字体设计单位
|
||||
// VerticalMetrics依次返回非负的上升高度、下降高度和行间隙
|
||||
// Write返回独立的SFNT数据,调用方不得修改字体实例
|
||||
type FontMetrics interface {
|
||||
UnitsPerEm() uint16
|
||||
NumGlyphs() uint16
|
||||
GlyphIndex(rune) uint16
|
||||
GlyphAdvance(uint16) uint16
|
||||
VerticalMetrics() (uint16, uint16, uint16)
|
||||
Write() []byte
|
||||
}
|
||||
|
||||
// ResolvedFont 保存字体来源和只读字体数据,不将外部字体写入文档
|
||||
type ResolvedFont struct {
|
||||
Data []byte
|
||||
Source string
|
||||
Face *FontFace
|
||||
Exact bool
|
||||
}
|
||||
|
||||
// FontBackend 统一字体来源与度量,exact禁止使用无关回退字体
|
||||
// ResolveFont不得递归调用Renderer.ResolveFont,OpenFont不得修改输入数据
|
||||
type FontBackend interface {
|
||||
Backend
|
||||
ResolveFont(renderer *Renderer, id string, exact bool) (ResolvedFont, error)
|
||||
OpenFont(data []byte) (FontMetrics, error)
|
||||
}
|
||||
|
||||
// resolvedFontKey 区分只读资源的字体标识与精确匹配要求
|
||||
type resolvedFontKey struct {
|
||||
id string
|
||||
exact bool
|
||||
definition *Font
|
||||
}
|
||||
|
||||
// resolvedFontResult 缓存字体解析结果及不可用原因
|
||||
type resolvedFontResult struct {
|
||||
font ResolvedFont
|
||||
err error
|
||||
}
|
||||
|
||||
// ResolveFont 使用配置的字体后端解析资源,保留精确匹配与回退的区别
|
||||
// 入参: id 字体资源标识, exact 是否要求精确匹配
|
||||
// 返回: ResolvedFont 字体与来源, error 解析错误
|
||||
func (r *Renderer) ResolveFont(id string, exact bool) (ResolvedFont, error) {
|
||||
if r.backends.Fonts == nil {
|
||||
return ResolvedFont{}, fmt.Errorf("fonts: %w", ErrBackendUnavailable)
|
||||
}
|
||||
key := resolvedFontKey{id: id, exact: exact, definition: r.Reader.fontCache[id]}
|
||||
if cached, ok := r.resolvedFonts[key]; ok {
|
||||
return cached.font, cached.err
|
||||
}
|
||||
resolved, err := r.backends.Fonts.ResolveFont(r, id, exact)
|
||||
key.definition = r.Reader.fontCache[id]
|
||||
r.resolvedFonts[key] = resolvedFontResult{font: resolved, err: err}
|
||||
return resolved, err
|
||||
}
|
||||
+4
-5
@@ -298,11 +298,10 @@ func (r *Renderer) fontInfo(font Font) FontInfo {
|
||||
}
|
||||
return info
|
||||
}
|
||||
if source, family := r.fontSourceMatch(font.ID, &font); family != nil {
|
||||
info.Matched = source.name
|
||||
face := fontFaceInfo(family.Face(12, canvasFontStyle(&font)).Font.SFNT.Tables["name"], source.face)
|
||||
info.MatchedFace = &face
|
||||
if source.exact {
|
||||
if resolved, err := r.ResolveFont(font.ID, false); err == nil && resolved.Source != "" {
|
||||
info.Matched = resolved.Source
|
||||
info.MatchedFace = resolved.Face
|
||||
if resolved.Exact {
|
||||
info.Status = FontStatusMatched
|
||||
info.Detail = "使用外部字体文件"
|
||||
} else {
|
||||
|
||||
@@ -704,6 +704,9 @@ func serializeOTF(tables map[string][]byte) ([]byte, error) {
|
||||
pad := (4 - (len(data) % 4)) % 4
|
||||
padded := make([]byte, len(data)+pad)
|
||||
copy(padded, data)
|
||||
if tag == "head" && len(data) >= 12 {
|
||||
clear(padded[8:12])
|
||||
}
|
||||
cs := calcTableChecksum(padded)
|
||||
records = append(records, otfTableRecord{tag, cs, offset, len(data), padded})
|
||||
offset += len(padded)
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// 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"
|
||||
|
||||
// GeometryOperation 指定路径填充区域的布尔运算
|
||||
type GeometryOperation uint8
|
||||
|
||||
const (
|
||||
GeometryIntersect GeometryOperation = iota
|
||||
GeometryUnion
|
||||
GeometrySubtract
|
||||
GeometryXor
|
||||
)
|
||||
|
||||
// StrokeOptions 描述毫米单位的描边,Cap和Join使用OFD名称
|
||||
// Tolerance为曲线展开误差,0采用后端默认精度,Dashes和DashOffset为绝对长度
|
||||
type StrokeOptions struct {
|
||||
Width float64
|
||||
Cap, Join string
|
||||
MiterLimit, DashOffset, Tolerance float64
|
||||
Dashes []float64
|
||||
}
|
||||
|
||||
// GeometryBackend 操作库自有路径,坐标以页面左上角为原点,不修改任何输入
|
||||
// Clip返回nil表示不裁剪,空路径指针表示完全裁去,矩阵映射到页面坐标
|
||||
type GeometryBackend interface {
|
||||
Backend
|
||||
Path(object PathObject) (GeometryPath, error)
|
||||
Region(region *Region, matrix Matrix) (GeometryPath, error)
|
||||
Bounds(path GeometryPath) (Box, error)
|
||||
Transform(path GeometryPath, matrix Matrix) (GeometryPath, error)
|
||||
Normalize(path GeometryPath, evenOdd bool) (GeometryPath, error)
|
||||
Combine(left, right GeometryPath, operation GeometryOperation) (GeometryPath, error)
|
||||
Stroke(path GeometryPath, options StrokeOptions) (GeometryPath, error)
|
||||
Clip(renderer *Renderer, clips *Clips, matrix Matrix, parent *GeometryPath) (*GeometryPath, error)
|
||||
}
|
||||
|
||||
// Geometry 返回当前几何后端,不隐式恢复默认实现
|
||||
// 返回: GeometryBackend 几何后端, error 未配置错误
|
||||
func (r *Renderer) Geometry() (GeometryBackend, error) {
|
||||
if r.backends.Geometry == nil {
|
||||
return nil, fmt.Errorf("geometry: %w", ErrBackendUnavailable)
|
||||
}
|
||||
return r.backends.Geometry, nil
|
||||
}
|
||||
|
||||
// Geometry 返回编辑器配置的几何后端
|
||||
// 返回: GeometryBackend 几何后端, error 未配置错误
|
||||
func (e *Editor) Geometry() (GeometryBackend, error) {
|
||||
if e.backends.Geometry == nil {
|
||||
return nil, fmt.Errorf("geometry: %w", ErrBackendUnavailable)
|
||||
}
|
||||
return e.backends.Geometry, nil
|
||||
}
|
||||
|
||||
// PathOutline 使用配置的几何后端生成页面坐标中的SVG轮廓
|
||||
// 入参: object 路径对象
|
||||
// 返回: string SVG路径, error 几何或编码错误
|
||||
func (r *Renderer) PathOutline(object PathObject) (string, error) {
|
||||
geometry, err := r.Geometry()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
path, err := geometry.Path(object)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return path.SVG()
|
||||
}
|
||||
|
||||
// geometryRectangle 构建页面坐标矩形,保留输入精度
|
||||
// 入参: box 矩形范围
|
||||
// 返回: GeometryPath 闭合路径
|
||||
func geometryRectangle(box Box) GeometryPath {
|
||||
return GeometryPath{
|
||||
{Verb: GeometryMove, End: Point{X: box.X, Y: box.Y}},
|
||||
{Verb: GeometryLine, End: Point{X: box.X + box.W, Y: box.Y}},
|
||||
{Verb: GeometryLine, End: Point{X: box.X + box.W, Y: box.Y + box.H}},
|
||||
{Verb: GeometryLine, End: Point{X: box.X, Y: box.Y + box.H}},
|
||||
{Verb: GeometryClose, End: Point{X: box.X, Y: box.Y}},
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,11 @@
|
||||
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GeometryPath 保存独立于绘图库的几何路径,不扁平化曲线或舍入坐标
|
||||
// 坐标以左上角为原点,单位为毫米,子路径从Move开始,Close回到当前子路径起点
|
||||
type GeometryPath []GeometrySegment
|
||||
@@ -40,3 +45,69 @@ type GeometrySegment struct {
|
||||
Rotation float64
|
||||
Large, Sweep bool
|
||||
}
|
||||
|
||||
// SVG 将几何路径编码为SVG路径,不改变曲线或坐标精度
|
||||
// 返回: string SVG路径, error 不支持的路径指令
|
||||
func (p GeometryPath) SVG() (string, error) {
|
||||
return p.encode(true)
|
||||
}
|
||||
|
||||
// OFD 将几何路径编码为OFD紧缩路径
|
||||
// 返回: string 紧缩路径, error 不支持的路径指令
|
||||
func (p GeometryPath) OFD() (string, error) {
|
||||
return p.encode(false)
|
||||
}
|
||||
|
||||
// encode 按目标格式写入路径,圆弧标志采用两种格式均支持的0和1
|
||||
// 入参: svg 是否使用SVG指令
|
||||
// 返回: string 路径数据, error 不支持的路径指令
|
||||
func (p GeometryPath) encode(svg bool) (string, error) {
|
||||
var value strings.Builder
|
||||
for _, segment := range p {
|
||||
x, y := ofdNumber(segment.End.X), ofdNumber(segment.End.Y)
|
||||
switch segment.Verb {
|
||||
case GeometryMove:
|
||||
fmt.Fprintf(&value, "M %s %s ", x, y)
|
||||
case GeometryLine:
|
||||
fmt.Fprintf(&value, "L %s %s ", x, y)
|
||||
case GeometryQuad:
|
||||
fmt.Fprintf(&value, "Q %s %s %s %s ", ofdNumber(segment.Control1.X), ofdNumber(segment.Control1.Y), x, y)
|
||||
case GeometryCubic:
|
||||
command := "B"
|
||||
if svg {
|
||||
command = "C"
|
||||
}
|
||||
fmt.Fprintf(&value, "%s %s %s %s %s %s %s ", command, ofdNumber(segment.Control1.X), ofdNumber(segment.Control1.Y), ofdNumber(segment.Control2.X), ofdNumber(segment.Control2.Y), x, y)
|
||||
case GeometryArc:
|
||||
large, sweep := 0, 0
|
||||
if segment.Large {
|
||||
large = 1
|
||||
}
|
||||
if segment.Sweep {
|
||||
sweep = 1
|
||||
}
|
||||
fmt.Fprintf(&value, "A %s %s %s %d %d %s %s ", ofdNumber(segment.RadiusX), ofdNumber(segment.RadiusY), ofdNumber(segment.Rotation), large, sweep, x, y)
|
||||
case GeometryClose:
|
||||
if svg {
|
||||
value.WriteString("Z ")
|
||||
} else {
|
||||
value.WriteString("C ")
|
||||
}
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported geometry command %d", segment.Verb)
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(value.String()), nil
|
||||
}
|
||||
|
||||
// geometryClipPath 将页面路径转换为标准填充裁剪对象
|
||||
// 入参: path 页面毫米路径
|
||||
// 返回: PathObject 裁剪路径, error 无效指令错误
|
||||
func geometryClipPath(path GeometryPath) (PathObject, error) {
|
||||
data, err := path.OFD()
|
||||
if err != nil {
|
||||
return PathObject{}, err
|
||||
}
|
||||
fill, stroke := true, false
|
||||
return PathObject{Boundary: "0 0 1 1", Fill: &fill, Stroke: &stroke, AbbreviatedData: data}, nil
|
||||
}
|
||||
+307
@@ -21,10 +21,12 @@ import (
|
||||
"image"
|
||||
"image/color"
|
||||
_ "image/gif"
|
||||
"math"
|
||||
|
||||
canvasimage "github.com/tdewolff/canvas/image"
|
||||
_ "github.com/xiaoqidun/jbig2"
|
||||
_ "golang.org/x/image/bmp"
|
||||
"golang.org/x/image/draw"
|
||||
_ "golang.org/x/image/tiff"
|
||||
)
|
||||
|
||||
@@ -175,3 +177,308 @@ func parseBMPHeader(data []byte) (int, int, int, uint16, uint32, error) {
|
||||
}
|
||||
return offset, width, height, bpp, compression, nil
|
||||
}
|
||||
|
||||
// imageWithMask 应用图片蒙版
|
||||
// 入参: img 图片对象, mask 蒙版图片
|
||||
// 返回: image.Image 蒙版处理后的图片对象
|
||||
func imageWithMask(img, mask image.Image) image.Image {
|
||||
if img == nil || mask == nil {
|
||||
return img
|
||||
}
|
||||
bounds := img.Bounds()
|
||||
if bounds.Empty() || mask.Bounds().Empty() {
|
||||
return img
|
||||
}
|
||||
source := imagePixelSource(img)
|
||||
maskSource := imagePixelSource(mask)
|
||||
maskBounds := maskSource.Bounds()
|
||||
var opacity image.Image = maskSource
|
||||
if bounds.Size() != maskBounds.Size() {
|
||||
resized := image.NewGray(bounds)
|
||||
draw.CatmullRom.Scale(resized, bounds, maskSource, maskBounds, draw.Src, nil)
|
||||
opacity = resized
|
||||
maskBounds = bounds
|
||||
}
|
||||
out := image.NewNRGBA(bounds)
|
||||
if src, ok := source.(*image.NRGBA); ok {
|
||||
for y := 0; y < bounds.Dy(); y++ {
|
||||
offset := src.PixOffset(bounds.Min.X, bounds.Min.Y+y)
|
||||
row := out.Pix[y*out.Stride : (y+1)*out.Stride]
|
||||
copy(row, src.Pix[offset:offset+len(row)])
|
||||
for x := 3; x < len(row); x += 4 {
|
||||
a := imageGrayAt(opacity, maskBounds.Min.X+x/4, maskBounds.Min.Y+y).Y
|
||||
row[x] = uint8(int(row[x]) * int(a) / 255)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
mx := maskBounds.Min.X + x - bounds.Min.X
|
||||
my := maskBounds.Min.Y + y - bounds.Min.Y
|
||||
a := imageGrayAt(opacity, mx, my).Y
|
||||
c := imageNRGBAAt(source, x, y)
|
||||
c.A = uint8(int(c.A) * int(a) / 255)
|
||||
out.SetNRGBA(x, y, c)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// imageWithAlpha 合并图片透明度
|
||||
// 入参: img 图片对象, alpha 对象透明度
|
||||
// 返回: image.Image 合并后的图片对象
|
||||
func imageWithAlpha(img image.Image, alpha *int) image.Image {
|
||||
if img == nil || alpha == nil {
|
||||
return img
|
||||
}
|
||||
a := clampColor(*alpha)
|
||||
if a == 255 {
|
||||
return img
|
||||
}
|
||||
bounds := img.Bounds()
|
||||
out := image.NewNRGBA(bounds)
|
||||
source := imagePixelSource(img)
|
||||
if src, ok := source.(*image.NRGBA); ok {
|
||||
for y := 0; y < bounds.Dy(); y++ {
|
||||
offset := src.PixOffset(bounds.Min.X, bounds.Min.Y+y)
|
||||
row := out.Pix[y*out.Stride : (y+1)*out.Stride]
|
||||
copy(row, src.Pix[offset:offset+len(row)])
|
||||
for x := 3; x < len(row); x += 4 {
|
||||
row[x] = uint8(int(row[x]) * a / 255)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
c := imageNRGBAAt(source, x, y)
|
||||
c.A = uint8(int(c.A) * a / 255)
|
||||
out.SetNRGBA(x, y, c)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// imagePixelSource 获取图片像素源
|
||||
// 入参: img 图片对象
|
||||
// 返回: image.Image 图片像素源
|
||||
func imagePixelSource(img image.Image) image.Image {
|
||||
if src, ok := img.(interface{ Image() (image.Image, error) }); ok {
|
||||
if decoded, err := src.Image(); err == nil {
|
||||
return decoded
|
||||
}
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
// imageGrayAt 获取图片灰度像素
|
||||
// 入参: img 图片对象, x X坐标, y Y坐标
|
||||
// 返回: color.Gray 灰度像素
|
||||
func imageGrayAt(img image.Image, x, y int) color.Gray {
|
||||
if src, ok := img.(*image.Gray); ok {
|
||||
return src.GrayAt(x, y)
|
||||
}
|
||||
if src, ok := img.(*image.Paletted); ok {
|
||||
return color.GrayModel.Convert(src.At(x, y)).(color.Gray)
|
||||
}
|
||||
if src, ok := img.(image.RGBA64Image); ok {
|
||||
r, g, b, _ := src.RGBA64At(x, y).RGBA()
|
||||
return color.Gray{Y: uint8((19595*r + 38470*g + 7471*b + 1<<15) >> 24)}
|
||||
}
|
||||
return color.GrayModel.Convert(img.At(x, y)).(color.Gray)
|
||||
}
|
||||
|
||||
// imageNRGBAAt 获取图片NRGBA像素
|
||||
// 入参: img 图片对象, x X坐标, y Y坐标
|
||||
// 返回: color.NRGBA NRGBA像素
|
||||
func imageNRGBAAt(img image.Image, x, y int) color.NRGBA {
|
||||
if src, ok := img.(*image.NRGBA); ok {
|
||||
return src.NRGBAAt(x, y)
|
||||
}
|
||||
if src, ok := img.(*image.YCbCr); ok {
|
||||
r, g, b, _ := src.YCbCrAt(x, y).RGBA()
|
||||
return color.NRGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: 255}
|
||||
}
|
||||
if src, ok := img.(*image.RGBA); ok {
|
||||
c := src.RGBA64At(x, y)
|
||||
if c.A == 0 {
|
||||
return color.NRGBA{}
|
||||
}
|
||||
r, g, b, a := uint32(c.R), uint32(c.G), uint32(c.B), uint32(c.A)
|
||||
if a != 0xffff {
|
||||
r = r * 0xffff / a
|
||||
g = g * 0xffff / a
|
||||
b = b * 0xffff / a
|
||||
}
|
||||
return color.NRGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)}
|
||||
}
|
||||
return color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
|
||||
}
|
||||
|
||||
// transparentEdgeColor 获取透明像素相邻的可见颜色
|
||||
// 入参: img 图片对象, x X坐标, y Y坐标
|
||||
// 返回: color.NRGBA 颜色, bool 是否存在
|
||||
func transparentEdgeColor(img *image.NRGBA, x, y int) (color.NRGBA, bool) {
|
||||
bounds := img.Bounds()
|
||||
var best color.NRGBA
|
||||
for dy := -1; dy <= 1; dy++ {
|
||||
for dx := -1; dx <= 1; dx++ {
|
||||
if dx == 0 && dy == 0 {
|
||||
continue
|
||||
}
|
||||
nx, ny := x+dx, y+dy
|
||||
if nx < bounds.Min.X || nx >= bounds.Max.X || ny < bounds.Min.Y || ny >= bounds.Max.Y {
|
||||
continue
|
||||
}
|
||||
c := img.NRGBAAt(nx, ny)
|
||||
if c.A > best.A {
|
||||
best = c
|
||||
}
|
||||
}
|
||||
}
|
||||
if best.A == 0 {
|
||||
return color.NRGBA{}, false
|
||||
}
|
||||
best.A = 1
|
||||
return best, true
|
||||
}
|
||||
|
||||
// transparentPaddingColor 获取透明补齐颜色
|
||||
// 入参: c 边缘颜色
|
||||
// 返回: color.NRGBA 补齐颜色
|
||||
func transparentPaddingColor(c color.NRGBA) color.NRGBA {
|
||||
c.A = 1
|
||||
return c
|
||||
}
|
||||
|
||||
// clipStampImage 裁剪印章图像
|
||||
// 入参: img 印章图像, box 印章区域, clip 裁剪区域
|
||||
// 返回: image.Image 裁剪后的印章图像
|
||||
func clipStampImage(img image.Image, box, clip Box) image.Image {
|
||||
bounds := img.Bounds()
|
||||
x0 := int(math.Floor(clip.X / box.W * float64(bounds.Dx())))
|
||||
y0 := int(math.Floor(clip.Y / box.H * float64(bounds.Dy())))
|
||||
x1 := int(math.Ceil((clip.X + clip.W) / box.W * float64(bounds.Dx())))
|
||||
y1 := int(math.Ceil((clip.Y + clip.H) / box.H * float64(bounds.Dy())))
|
||||
out := image.NewNRGBA(image.Rect(0, 0, x1-x0, y1-y0))
|
||||
draw.Draw(out, out.Bounds(), img, image.Pt(bounds.Min.X+x0, bounds.Min.Y+y0), draw.Src)
|
||||
return out
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// imageWithTransparentEdge 补齐透明图片边缘颜色
|
||||
// 入参: img 图片对象
|
||||
// 返回: image.Image 补齐后的图片对象, int 补齐像素数
|
||||
func imageWithTransparentEdge(img image.Image) (image.Image, int) {
|
||||
if img == nil {
|
||||
return img, 0
|
||||
}
|
||||
bounds := img.Bounds()
|
||||
w, h := bounds.Dx(), bounds.Dy()
|
||||
if w == 0 || h == 0 {
|
||||
return img, 0
|
||||
}
|
||||
if src, ok := img.(*canvasimage.Image); ok && src.Mimetype == "image/jpeg" && src.Mask == nil {
|
||||
return img, 0
|
||||
}
|
||||
source := imagePixelSource(img)
|
||||
if opaque, ok := source.(interface{ Opaque() bool }); ok && opaque.Opaque() {
|
||||
return img, 0
|
||||
}
|
||||
hasZero, hasVisible := false, false
|
||||
src, ok := source.(*image.NRGBA)
|
||||
if ok {
|
||||
srcBounds := src.Bounds()
|
||||
hasZero = src.Pix[3] == 0
|
||||
hasVisible = !hasZero
|
||||
scan:
|
||||
for y := srcBounds.Min.Y; y < srcBounds.Max.Y; y++ {
|
||||
offset := src.PixOffset(srcBounds.Min.X, y) + 3
|
||||
for x := 0; x < w; x++ {
|
||||
if (src.Pix[offset] == 0) != hasZero {
|
||||
hasZero, hasVisible = true, true
|
||||
break scan
|
||||
}
|
||||
offset += 4
|
||||
}
|
||||
}
|
||||
} else {
|
||||
src = image.NewNRGBA(image.Rect(0, 0, w, h))
|
||||
for y := 0; y < h; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
c := imageNRGBAAt(source, bounds.Min.X+x, bounds.Min.Y+y)
|
||||
src.SetNRGBA(x, y, c)
|
||||
if c.A == 0 {
|
||||
hasZero = true
|
||||
} else {
|
||||
hasVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hasZero || !hasVisible {
|
||||
return img, 0
|
||||
}
|
||||
srcBounds := src.Bounds()
|
||||
out := image.NewNRGBA(image.Rect(0, 0, w+2, h+2))
|
||||
draw.Draw(out, out.Bounds().Inset(1), src, srcBounds.Min, draw.Src)
|
||||
for y := 0; y < h; y++ {
|
||||
sy := srcBounds.Min.Y + y
|
||||
offset := src.PixOffset(srcBounds.Min.X, sy) + 3
|
||||
for x := 0; x < w; x++ {
|
||||
if src.Pix[offset] == 0 {
|
||||
if edge, ok := transparentEdgeColor(src, srcBounds.Min.X+x, sy); ok {
|
||||
out.SetNRGBA(x+1, y+1, edge)
|
||||
}
|
||||
}
|
||||
offset += 4
|
||||
}
|
||||
}
|
||||
for x := 0; x < w; x++ {
|
||||
out.SetNRGBA(x+1, 0, transparentPaddingColor(out.NRGBAAt(x+1, 1)))
|
||||
out.SetNRGBA(x+1, h+1, transparentPaddingColor(out.NRGBAAt(x+1, h)))
|
||||
}
|
||||
for y := 0; y < h; y++ {
|
||||
out.SetNRGBA(0, y+1, transparentPaddingColor(out.NRGBAAt(1, y+1)))
|
||||
out.SetNRGBA(w+1, y+1, transparentPaddingColor(out.NRGBAAt(w, y+1)))
|
||||
}
|
||||
out.SetNRGBA(0, 0, transparentPaddingColor(out.NRGBAAt(1, 1)))
|
||||
out.SetNRGBA(w+1, 0, transparentPaddingColor(out.NRGBAAt(w, 1)))
|
||||
out.SetNRGBA(0, h+1, transparentPaddingColor(out.NRGBAAt(1, h)))
|
||||
out.SetNRGBA(w+1, h+1, transparentPaddingColor(out.NRGBAAt(w, h)))
|
||||
return out, 1
|
||||
}
|
||||
+46
-22
@@ -17,8 +17,6 @@ package ofdgo
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
// InkPoint 手写采样点,X和Y为毫米,Pressure为0到1的压力
|
||||
@@ -36,24 +34,30 @@ func NewInk(points []InkPoint, width float64, pressure bool) (PathObject, error)
|
||||
if len(points) == 0 || !finite(width) || width <= 0 {
|
||||
return PathObject{}, fmt.Errorf("ink requires points and a positive finite width")
|
||||
}
|
||||
path := &canvas.Path{}
|
||||
var path GeometryPath
|
||||
stationary := true
|
||||
x0, y0, x1, y1 := math.Inf(1), math.Inf(1), math.Inf(-1), math.Inf(-1)
|
||||
for i, point := range points {
|
||||
if !finite(point.X) || !finite(point.Y) || !finite(point.Pressure) || point.Pressure < 0 || point.Pressure > 1 {
|
||||
return PathObject{}, fmt.Errorf("invalid ink point %d", i)
|
||||
}
|
||||
stationary = stationary && point.X == points[0].X && point.Y == points[0].Y
|
||||
if !pressure {
|
||||
if i == 0 {
|
||||
path.MoveTo(point.X, point.Y)
|
||||
} else {
|
||||
path.LineTo(point.X, point.Y)
|
||||
radius := 0.0
|
||||
if pressure {
|
||||
radius = width * point.Pressure / 2
|
||||
}
|
||||
x0, y0 = math.Min(x0, point.X-radius), math.Min(y0, point.Y-radius)
|
||||
x1, y1 = math.Max(x1, point.X+radius), math.Max(y1, point.Y+radius)
|
||||
if !pressure {
|
||||
verb := GeometryLine
|
||||
if i == 0 {
|
||||
verb = GeometryMove
|
||||
}
|
||||
path = append(path, GeometrySegment{Verb: verb, End: Point{X: point.X, Y: point.Y}})
|
||||
continue
|
||||
}
|
||||
radius := width * point.Pressure / 2
|
||||
if radius > 0 {
|
||||
path = path.Append(canvas.Circle(radius).Translate(point.X, point.Y))
|
||||
path = append(path, inkCircle(point.X, point.Y, radius)...)
|
||||
}
|
||||
if i == 0 {
|
||||
continue
|
||||
@@ -69,25 +73,32 @@ func NewInk(points []InkPoint, width float64, pressure bool) (PathObject, error)
|
||||
a := (r - radius) / length
|
||||
b := math.Sqrt(1 - a*a)
|
||||
nx, ny, mx, my := a*x-b*y, a*y+b*x, a*x+b*y, a*y-b*x
|
||||
segment := &canvas.Path{}
|
||||
segment.MoveTo(previous.X+r*mx, previous.Y+r*my)
|
||||
segment.LineTo(point.X+radius*mx, point.Y+radius*my)
|
||||
segment.LineTo(point.X+radius*nx, point.Y+radius*ny)
|
||||
segment.LineTo(previous.X+r*nx, previous.Y+r*ny)
|
||||
segment.Close()
|
||||
path = path.Append(segment)
|
||||
start := Point{X: previous.X + r*mx, Y: previous.Y + r*my}
|
||||
path = append(path,
|
||||
GeometrySegment{Verb: GeometryMove, End: start},
|
||||
GeometrySegment{Verb: GeometryLine, End: Point{X: point.X + radius*mx, Y: point.Y + radius*my}},
|
||||
GeometrySegment{Verb: GeometryLine, End: Point{X: point.X + radius*nx, Y: point.Y + radius*ny}},
|
||||
GeometrySegment{Verb: GeometryLine, End: Point{X: previous.X + r*nx, Y: previous.Y + r*ny}},
|
||||
GeometrySegment{Verb: GeometryClose, End: start})
|
||||
}
|
||||
filled := pressure || stationary
|
||||
if !pressure && stationary {
|
||||
path = canvas.Circle(width/2).Translate(points[0].X, points[0].Y)
|
||||
path = inkCircle(points[0].X, points[0].Y, width/2)
|
||||
x0, y0, x1, y1 = x0-width/2, y0-width/2, x1+width/2, y1+width/2
|
||||
}
|
||||
if path.Empty() {
|
||||
if len(path) == 0 {
|
||||
return PathObject{}, fmt.Errorf("ink has no visible stroke")
|
||||
}
|
||||
box := path.Bounds()
|
||||
padding := width / 2
|
||||
boundary := Box{X: box.X0 - padding, Y: box.Y0 - padding, W: math.Max(box.W(), width) + width, H: math.Max(box.H(), width) + width}
|
||||
object := editorClipPath(path.Translate(-boundary.X, -boundary.Y))
|
||||
boundary := Box{X: x0 - padding, Y: y0 - padding, W: math.Max(x1-x0, width) + width, H: math.Max(y1-y0, width) + width}
|
||||
for i := range path {
|
||||
path[i].End.X -= boundary.X
|
||||
path[i].End.Y -= boundary.Y
|
||||
}
|
||||
object, err := geometryClipPath(path)
|
||||
if err != nil {
|
||||
return PathObject{}, err
|
||||
}
|
||||
object.Boundary = editorBoxString(boundary)
|
||||
stroke := !filled
|
||||
object.Fill, object.Stroke = &filled, &stroke
|
||||
@@ -99,3 +110,16 @@ func NewInk(points []InkPoint, width float64, pressure bool) (PathObject, error)
|
||||
}
|
||||
return object, nil
|
||||
}
|
||||
|
||||
// inkCircle 使用两个半圆弧构造笔迹圆点,不扁平化圆形轮廓
|
||||
// 入参: x、y 圆心, radius 半径
|
||||
// 返回: GeometryPath 圆形路径
|
||||
func inkCircle(x, y, radius float64) GeometryPath {
|
||||
start := Point{X: x + radius, Y: y}
|
||||
return GeometryPath{
|
||||
{Verb: GeometryMove, End: start},
|
||||
{Verb: GeometryArc, End: Point{X: x - radius, Y: y}, RadiusX: radius, RadiusY: radius, Sweep: true},
|
||||
{Verb: GeometryArc, End: start, RadiusX: radius, RadiusY: radius, Sweep: true},
|
||||
{Verb: GeometryClose, End: start},
|
||||
}
|
||||
}
|
||||
+13
-106
@@ -16,13 +16,17 @@ package ofdgo
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
// ResolveColor 按文档颜色空间和调色板解析颜色,返回预乘透明度的RGBA
|
||||
// 入参: value 颜色分量, index 调色板索引, space 颜色空间标识, alpha 透明度
|
||||
// 返回: color.RGBA 标准颜色
|
||||
func (r *Renderer) ResolveColor(value string, index *int, space string, alpha *int) color.RGBA {
|
||||
return colorToRGBA(r.parseColorWithAlpha(value, index, space, alpha))
|
||||
}
|
||||
|
||||
// parseColorWithAlpha 解析带透明度的颜色
|
||||
// 入参: value 颜色值, index 调色板索引, space 颜色空间标识, alpha 透明度
|
||||
// 返回: color.Color 颜色对象
|
||||
@@ -180,25 +184,6 @@ func (r *Renderer) parseFillColor(fillColor *FillColor) color.Color {
|
||||
return r.parseColorWithAlpha(fillColor.Value, fillColor.Index, fillColor.ColorSpace, fillColor.Alpha)
|
||||
}
|
||||
|
||||
// parseFillPaint 解析填充画刷
|
||||
// 入参: fillColor 填充颜色节点, x X坐标, y Y坐标, pageH 页面高度
|
||||
// 返回: any 填充画刷
|
||||
func (r *Renderer) parseFillPaint(fillColor *FillColor, x, y, pageH float64) any {
|
||||
if fillColor == nil {
|
||||
return nil
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// parseStrokeColor 解析勾边颜色
|
||||
// 入参: strokeColor 勾边颜色节点
|
||||
// 返回: color.Color 颜色对象
|
||||
@@ -206,13 +191,6 @@ func (r *Renderer) parseStrokeColor(strokeColor *StrokeColor) color.Color {
|
||||
return r.parseFillColor((*FillColor)(strokeColor))
|
||||
}
|
||||
|
||||
// parseStrokePaint 解析勾边画刷
|
||||
// 入参: strokeColor 勾边颜色节点, x X坐标, y Y坐标, pageH 页面高度
|
||||
// 返回: any 勾边画刷
|
||||
func (r *Renderer) parseStrokePaint(strokeColor *StrokeColor, x, y, pageH float64) any {
|
||||
return r.parseFillPaint((*FillColor)(strokeColor), x, y, pageH)
|
||||
}
|
||||
|
||||
// patternPaint 底纹画刷
|
||||
type patternPaint struct {
|
||||
*Pattern
|
||||
@@ -241,11 +219,12 @@ func (r *Renderer) parseShdColor(segments []ShdSegment, alpha *int) color.Color
|
||||
return r.parseColorWithAlpha(c.Value, c.Index, c.ColorSpace, mergeAlpha(c.Alpha, alpha))
|
||||
}
|
||||
|
||||
// parseShdSegments 解析渐变分段
|
||||
// GradientStops 解析OFD渐变分段的位置与透明度,返回独立数据并保留原分段顺序
|
||||
// GradientStops 解析OFD渐变分段的位置与透明度,返回独立数据并保留原分段顺序
|
||||
// 入参: segments 渐变分段, alpha 透明度
|
||||
// 返回: canvas.Grad 渐变分段
|
||||
func (r *Renderer) parseShdSegments(segments []ShdSegment, alpha *int) canvas.Grad {
|
||||
gradient := canvas.NewGradient()
|
||||
// 返回: []RasterStop 后端无关的渐变分段
|
||||
func (r *Renderer) GradientStops(segments []ShdSegment, alpha *int) []RasterStop {
|
||||
var gradient []RasterStop
|
||||
position, step := 0.0, 0.0
|
||||
for i, segment := range segments {
|
||||
if !segment.positionMissing {
|
||||
@@ -267,7 +246,7 @@ func (r *Renderer) parseShdSegments(segments []ShdSegment, alpha *int) canvas.Gr
|
||||
offset := position
|
||||
position += step
|
||||
segmentAlpha := mergeAlpha(segment.Color.Alpha, alpha)
|
||||
gradient.Add(offset, colorToRGBA(r.parseColorWithAlpha(segment.Color.Value, segment.Color.Index, segment.Color.ColorSpace, segmentAlpha)))
|
||||
gradient = append(gradient, RasterStop{Offset: offset, Color: r.ResolveColor(segment.Color.Value, segment.Color.Index, segment.Color.ColorSpace, segmentAlpha)})
|
||||
}
|
||||
if len(gradient) == 0 {
|
||||
return nil
|
||||
@@ -275,78 +254,6 @@ func (r *Renderer) parseShdSegments(segments []ShdSegment, alpha *int) canvas.Gr
|
||||
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
|
||||
}
|
||||
start := parseFloats(axialShd.StartPoint)
|
||||
end := parseFloats(axialShd.EndPoint)
|
||||
if len(start) < 2 || len(end) < 2 {
|
||||
return nil
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// axialShdClip 获取轴向渐变的延伸裁剪区域
|
||||
// 入参: gradient 轴向渐变, extend 延伸方向, bounds 可见区域
|
||||
// 返回: *canvas.Path 裁剪区域
|
||||
func axialShdClip(gradient *canvas.LinearGradient, extend int, bounds canvas.Rect) *canvas.Path {
|
||||
if extend == 3 {
|
||||
return nil
|
||||
}
|
||||
d := gradient.End.Sub(gradient.Start)
|
||||
axis := canvas.Matrix{{d.X, -d.Y, gradient.Start.X}, {d.Y, d.X, gradient.Start.Y}}
|
||||
area := bounds.Transform(axis.Inv())
|
||||
if extend&1 == 0 {
|
||||
area.X0 = math.Max(area.X0, 0)
|
||||
}
|
||||
if extend&2 == 0 {
|
||||
area.X1 = math.Min(area.X1, 1)
|
||||
}
|
||||
if area.X1 <= area.X0 {
|
||||
return &canvas.Path{}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// colorToRGBA 转换颜色对象
|
||||
// 入参: c 颜色对象
|
||||
// 返回: color.RGBA RGBA颜色
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
// 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
|
||||
|
||||
// RenderPage 编译页面为与绘图库无关的只读绘制数据
|
||||
// 入参: page 页面内容
|
||||
// 返回: *RasterPage 绘制页面, error 错误信息
|
||||
func (r *Renderer) RenderPage(page *PageContent) (*RasterPage, error) {
|
||||
return r.CompilePage(page)
|
||||
}
|
||||
|
||||
// GetPageBox 获取页面物理区域
|
||||
// 入参: page 页面内容
|
||||
// 返回: Box 区域, error 错误信息
|
||||
func (r *Renderer) GetPageBox(page *PageContent) (Box, error) {
|
||||
area, err := r.Reader.resolvePageArea(page.Area, page.Template)
|
||||
if err != nil {
|
||||
return Box{}, err
|
||||
}
|
||||
boxStr := area.PhysicalBox
|
||||
if boxStr == "" {
|
||||
boxStr = area.ApplicationBox
|
||||
}
|
||||
if boxStr == "" {
|
||||
boxStr = area.ContentBox
|
||||
}
|
||||
if boxStr == "" {
|
||||
boxStr = "0 0 210 297"
|
||||
}
|
||||
return ParseBox(boxStr)
|
||||
}
|
||||
|
||||
// PageLinks 获取页面、模板和可见注释的点击链接,包含复杂区域、组合图元和附件动作
|
||||
// 入参: page 页面内容
|
||||
// 返回: []PageLink 页面链接, error 错误信息
|
||||
func (r *Renderer) PageLinks(page *PageContent) ([]PageLink, error) {
|
||||
box, err := r.GetPageBox(page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sources := r.pageActionSources(page, box)
|
||||
if r.RenderAnnotations {
|
||||
sources = append(sources, r.annotationActionSources(r.Reader.Annots[page.ID])...)
|
||||
}
|
||||
var links []PageLink
|
||||
var bookmarks map[string]Dest
|
||||
for sourceIndex, source := range sources {
|
||||
start := len(links)
|
||||
for _, action := range source.Actions {
|
||||
if action.Event != "CLICK" {
|
||||
continue
|
||||
}
|
||||
box, path, err := r.actionLinkRegion(source, action)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if box.W <= 0 || box.H <= 0 {
|
||||
continue
|
||||
}
|
||||
link := PageLink{Box: box, Path: path}
|
||||
if action.Goto != nil {
|
||||
if bookmarks == nil {
|
||||
doc, err := r.Reader.Doc()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bookmarks = make(map[string]Dest, len(doc.Bookmarks.Bookmark))
|
||||
for _, bookmark := range doc.Bookmarks.Bookmark {
|
||||
bookmarks[bookmark.Name] = bookmark.Dest
|
||||
}
|
||||
}
|
||||
if dest := gotoDest(action.Goto, bookmarks); dest != nil {
|
||||
value := *dest
|
||||
link.Dest = &value
|
||||
links = append(links, link)
|
||||
}
|
||||
} else if action.URI != nil && action.URI.URI != "" {
|
||||
link.URI = resolveActionURI(*action.URI)
|
||||
links = append(links, link)
|
||||
} else if action.GotoA != nil {
|
||||
link.Attachment = action.GotoA.AttachID
|
||||
links = append(links, link)
|
||||
}
|
||||
}
|
||||
if len(links)-start > 1 {
|
||||
for i := start; i < len(links); i++ {
|
||||
links[i].Group = sourceIndex + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return links, nil
|
||||
}
|
||||
|
||||
// RenderPageByIndex 按索引渲染页面
|
||||
// 入参: index 页面索引,从0开始
|
||||
// 返回: *RasterPage 绘制页面, error 错误信息
|
||||
func (r *Renderer) RenderPageByIndex(index int) (*RasterPage, error) {
|
||||
page, err := r.Reader.PageContentByIndex(index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.RenderPage(page)
|
||||
}
|
||||
@@ -48,7 +48,10 @@ func (r *Renderer) AnnotationGeometry(annotation Annotation) (Box, []ObjectConto
|
||||
if action.Event != "CLICK" {
|
||||
continue
|
||||
}
|
||||
region, path := actionLinkRegion(source, action)
|
||||
region, path, err := r.actionLinkRegion(source, action)
|
||||
if err != nil {
|
||||
return Box{}, nil, err
|
||||
}
|
||||
if region.W <= 0 || region.H <= 0 {
|
||||
continue
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+41
-203
@@ -17,15 +17,15 @@ package ofdgo
|
||||
import (
|
||||
"image"
|
||||
"io/fs"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
// Renderer 渲染器实现
|
||||
// 通过NewRenderer创建,实例及共享的Reader需串行使用
|
||||
// OnPageText可选,接收页面绘制时同步提取的文字,不包含图案和签名外观
|
||||
// OnExportProgress可选,同步回报文档导出的已处理页数及总页数,返回错误则停止,页数完成不代表写入成功
|
||||
// TransparentBackground关闭页面白底,供嵌套图案和印章保持透明背景
|
||||
type Renderer struct {
|
||||
canvasFontState
|
||||
Reader *Reader
|
||||
DPI float64
|
||||
RenderAnnotations bool
|
||||
@@ -33,18 +33,6 @@ type Renderer struct {
|
||||
OnExportProgress func(completed, total int) error
|
||||
DrawParams map[string]*DrawParam
|
||||
CompositeGraphicUnits map[string]*CompositeGraphicUnit
|
||||
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
|
||||
templatePageCache map[string]*PageContent
|
||||
imageCache map[string]image.Image
|
||||
fontDirs []string
|
||||
@@ -53,6 +41,9 @@ type Renderer struct {
|
||||
pageText *PageText
|
||||
textOnly bool
|
||||
backends RenderBackends
|
||||
resolvedFonts map[resolvedFontKey]resolvedFontResult
|
||||
renderError error
|
||||
TransparentBackground bool
|
||||
}
|
||||
|
||||
// RendererOption 渲染器配置选项
|
||||
@@ -72,195 +63,6 @@ func (r *Renderer) FontSources() ([]string, []fs.FS) {
|
||||
return append([]string(nil), r.fontDirs...), append([]fs.FS(nil), r.fontFS...)
|
||||
}
|
||||
|
||||
// resetFontCache 重置字体匹配、加载和字形缓存
|
||||
func (r *Renderer) resetFontCache() {
|
||||
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)
|
||||
}
|
||||
|
||||
// RenderPage 编译页面为与绘图库无关的只读绘制数据
|
||||
// 入参: page 页面内容
|
||||
// 返回: *RasterPage 绘制页面, error 错误信息
|
||||
func (r *Renderer) RenderPage(page *PageContent) (*RasterPage, error) {
|
||||
return r.CompilePage(page)
|
||||
}
|
||||
|
||||
// renderPage 渲染特定页面内容
|
||||
// 入参: page 页面内容
|
||||
// 返回: *canvas.Canvas 画布实例, error 错误信息
|
||||
func (r *Renderer) renderPage(page *PageContent) (*canvas.Canvas, error) {
|
||||
box, err := r.GetPageBox(page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
width, height := box.W, box.H
|
||||
c := canvas.New(width, height)
|
||||
ctx := canvas.NewContext(c)
|
||||
if err := r.renderPageToContext(ctx, page, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// GetPageBox 获取页面物理区域
|
||||
// 入参: page 页面内容
|
||||
// 返回: Box 区域, error 错误信息
|
||||
func (r *Renderer) GetPageBox(page *PageContent) (Box, error) {
|
||||
area, err := r.Reader.resolvePageArea(page.Area, page.Template)
|
||||
if err != nil {
|
||||
return Box{}, err
|
||||
}
|
||||
boxStr := area.PhysicalBox
|
||||
if boxStr == "" {
|
||||
boxStr = area.ApplicationBox
|
||||
}
|
||||
if boxStr == "" {
|
||||
boxStr = area.ContentBox
|
||||
}
|
||||
if boxStr == "" {
|
||||
boxStr = "0 0 210 297"
|
||||
}
|
||||
return ParseBox(boxStr)
|
||||
}
|
||||
|
||||
// renderCanvasContext 渲染页面到默认解释器上下文
|
||||
// 入参: ctx 画布上下文, page 页面内容
|
||||
// 返回: error 错误信息
|
||||
func (r *Renderer) renderCanvasContext(ctx *canvas.Context, page *PageContent) error {
|
||||
renderer := *r
|
||||
renderer.decodeImages = true
|
||||
return renderer.renderPageToContext(ctx, page, true)
|
||||
}
|
||||
|
||||
// renderPageToContext 渲染页面到指定上下文
|
||||
// 入参: ctx 画布上下文, page 页面内容, drawBackground 是否绘制页面背景
|
||||
// 返回: error 错误信息
|
||||
func (r *Renderer) renderPageToContext(ctx *canvas.Context, page *PageContent, drawBackground bool) error {
|
||||
box, err := r.GetPageBox(page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.OnPageText != nil {
|
||||
renderer := *r
|
||||
renderer.pageText = &PageText{}
|
||||
r = &renderer
|
||||
}
|
||||
pageH := box.H
|
||||
if drawBackground {
|
||||
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 r.OnPageText != nil {
|
||||
r.OnPageText(page, r.pageText)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PageLinks 获取页面、模板和可见注释的点击链接,包含复杂区域、组合图元和附件动作
|
||||
// 入参: page 页面内容
|
||||
// 返回: []PageLink 页面链接, error 错误信息
|
||||
func (r *Renderer) PageLinks(page *PageContent) ([]PageLink, error) {
|
||||
box, err := r.GetPageBox(page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sources := r.pageActionSources(page, box)
|
||||
if r.RenderAnnotations {
|
||||
sources = append(sources, r.annotationActionSources(r.Reader.Annots[page.ID])...)
|
||||
}
|
||||
var links []PageLink
|
||||
var bookmarks map[string]Dest
|
||||
for sourceIndex, source := range sources {
|
||||
start := len(links)
|
||||
for _, action := range source.Actions {
|
||||
if action.Event != "CLICK" {
|
||||
continue
|
||||
}
|
||||
box, path := actionLinkRegion(source, action)
|
||||
if box.W <= 0 || box.H <= 0 {
|
||||
continue
|
||||
}
|
||||
link := PageLink{Box: box, Path: path}
|
||||
if action.Goto != nil {
|
||||
if bookmarks == nil {
|
||||
doc, err := r.Reader.Doc()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bookmarks = make(map[string]Dest, len(doc.Bookmarks.Bookmark))
|
||||
for _, bookmark := range doc.Bookmarks.Bookmark {
|
||||
bookmarks[bookmark.Name] = bookmark.Dest
|
||||
}
|
||||
}
|
||||
if dest := gotoDest(action.Goto, bookmarks); dest != nil {
|
||||
value := *dest
|
||||
link.Dest = &value
|
||||
links = append(links, link)
|
||||
}
|
||||
} else if action.URI != nil && action.URI.URI != "" {
|
||||
link.URI = resolveActionURI(*action.URI)
|
||||
links = append(links, link)
|
||||
} else if action.GotoA != nil {
|
||||
link.Attachment = action.GotoA.AttachID
|
||||
links = append(links, link)
|
||||
}
|
||||
}
|
||||
if len(links)-start > 1 {
|
||||
for i := start; i < len(links); i++ {
|
||||
links[i].Group = sourceIndex + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return links, nil
|
||||
}
|
||||
|
||||
// RenderPageByIndex 按索引渲染页面
|
||||
// 入参: index 页面索引,从0开始
|
||||
// 返回: *RasterPage 绘制页面, error 错误信息
|
||||
func (r *Renderer) RenderPageByIndex(index int) (*RasterPage, error) {
|
||||
page, err := r.Reader.PageContentByIndex(index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.RenderPage(page)
|
||||
}
|
||||
|
||||
// initCommon 初始化公共资源
|
||||
func (r *Renderer) initCommon() {
|
||||
r.fontFamily = canvas.NewFontFamily("default")
|
||||
r.defaultFontLoaded = r.loadDefaultFonts()
|
||||
}
|
||||
|
||||
// childRenderer 创建继承当前配置的子渲染器
|
||||
// 入参: reader 子阅读器
|
||||
// 返回: *Renderer 子渲染器
|
||||
@@ -268,6 +70,7 @@ func (r *Renderer) childRenderer(reader *Reader) *Renderer {
|
||||
opts := []RendererOption{
|
||||
WithDPI(r.DPI),
|
||||
WithAnnotations(r.RenderAnnotations),
|
||||
WithRenderBackends(r.backends),
|
||||
}
|
||||
if len(r.fontDirs) > 0 {
|
||||
opts = append(opts, WithFontDirs(r.fontDirs...))
|
||||
@@ -277,5 +80,40 @@ func (r *Renderer) childRenderer(reader *Reader) *Renderer {
|
||||
}
|
||||
renderer := NewRenderer(reader, opts...)
|
||||
renderer.decodeImages = r.decodeImages
|
||||
renderer.TransparentBackground = r.TransparentBackground
|
||||
return renderer
|
||||
}
|
||||
|
||||
// loadTemplate 加载模板页面并复用解析结果
|
||||
// 入参: templateID 模板ID
|
||||
// 返回: *PageContent 模板页面
|
||||
func (r *Renderer) loadTemplate(templateID string) *PageContent {
|
||||
if page := r.templatePageCache[templateID]; page != nil {
|
||||
return page
|
||||
}
|
||||
for _, tpl := range r.Reader.doc.CommonData.TemplatePage {
|
||||
if tpl.ID != templateID {
|
||||
continue
|
||||
}
|
||||
page, err := r.Reader.PageContent(Page{BaseLoc: tpl.BaseLoc})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
r.templatePageCache[templateID] = page
|
||||
return page
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// layerOrder 获取图层类型的绘制顺序
|
||||
// 入参: kind 图层类型
|
||||
// 返回: int 绘制顺序
|
||||
func layerOrder(kind string) int {
|
||||
switch kind {
|
||||
case "Background":
|
||||
return 0
|
||||
case "Foreground":
|
||||
return 2
|
||||
}
|
||||
return 1
|
||||
}
|
||||
File renamed without changes.
@@ -1,65 +0,0 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
// clipRenderer 收集图形和文字的绘制轮廓
|
||||
type clipRenderer struct {
|
||||
path *canvas.Path
|
||||
clip *canvas.Path
|
||||
}
|
||||
|
||||
// Size 返回不限定边界的裁剪画布尺寸
|
||||
// 返回: float64 宽度, float64 高度
|
||||
func (r *clipRenderer) Size() (float64, float64) {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// RenderPath 合并路径填充及描边轮廓
|
||||
// 入参: path 路径, style 绘制样式, m 变换矩阵
|
||||
func (r *clipRenderer) RenderPath(path *canvas.Path, style canvas.Style, m canvas.Matrix) {
|
||||
if style.HasFill() {
|
||||
r.add(applyClipPath(path.Copy().Transform(m), r.clip))
|
||||
}
|
||||
if style.HasStroke() {
|
||||
p := path.Dash(style.DashOffset, style.Dashes...).Stroke(style.StrokeWidth, style.StrokeCapper, style.StrokeJoiner, canvas.Tolerance)
|
||||
r.add(applyClipPath(p.Transform(m), r.clip))
|
||||
}
|
||||
}
|
||||
|
||||
// add 合并裁剪轮廓
|
||||
// 入参: path 裁剪路径
|
||||
func (r *clipRenderer) add(path *canvas.Path) {
|
||||
if r.path == nil {
|
||||
r.path = path
|
||||
} else {
|
||||
r.path = unionClipPath(r.path, path)
|
||||
}
|
||||
}
|
||||
|
||||
// RenderText 合并文字字形轮廓
|
||||
// 入参: text 文字, m 变换矩阵
|
||||
func (r *clipRenderer) RenderText(text *canvas.Text, m canvas.Matrix) {
|
||||
text.RenderTo(r, m, 0)
|
||||
}
|
||||
|
||||
// RenderImage 裁剪区不包含图像对象
|
||||
// 入参: img 图像, m 变换矩阵
|
||||
func (r *clipRenderer) RenderImage(img image.Image, m canvas.Matrix) {}
|
||||
@@ -0,0 +1,249 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
// clipRenderer 收集图形和文字的绘制轮廓
|
||||
type clipRenderer struct {
|
||||
path *canvas.Path
|
||||
clip *canvas.Path
|
||||
}
|
||||
|
||||
// Size 返回不限定边界的裁剪画布尺寸
|
||||
// 返回: float64 宽度, float64 高度
|
||||
func (r *clipRenderer) Size() (float64, float64) {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// RenderPath 合并路径填充及描边轮廓
|
||||
// 入参: path 路径, style 绘制样式, m 变换矩阵
|
||||
func (r *clipRenderer) RenderPath(path *canvas.Path, style canvas.Style, m canvas.Matrix) {
|
||||
if style.HasFill() {
|
||||
r.add(applyClipPath(path.Copy().Transform(m), r.clip))
|
||||
}
|
||||
if style.HasStroke() {
|
||||
p := path.Dash(style.DashOffset, style.Dashes...).Stroke(style.StrokeWidth, style.StrokeCapper, style.StrokeJoiner, canvas.Tolerance)
|
||||
r.add(applyClipPath(p.Transform(m), r.clip))
|
||||
}
|
||||
}
|
||||
|
||||
// add 合并裁剪轮廓
|
||||
// 入参: path 裁剪路径
|
||||
func (r *clipRenderer) add(path *canvas.Path) {
|
||||
if r.path == nil {
|
||||
r.path = path
|
||||
} else {
|
||||
r.path = unionClipPath(r.path, path)
|
||||
}
|
||||
}
|
||||
|
||||
// RenderText 合并文字字形轮廓
|
||||
// 入参: text 文字, m 变换矩阵
|
||||
func (r *clipRenderer) RenderText(text *canvas.Text, m canvas.Matrix) {
|
||||
text.RenderTo(r, m, 0)
|
||||
}
|
||||
|
||||
// RenderImage 裁剪区不包含图像对象
|
||||
// 入参: img 图像, m 变换矩阵
|
||||
func (r *clipRenderer) RenderImage(img image.Image, m canvas.Matrix) {}
|
||||
|
||||
// buildObjectClipPath 构建对象裁剪路径并应用父级变换
|
||||
// 入参: 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 !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},
|
||||
})
|
||||
}
|
||||
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 {
|
||||
renderer := &clipRenderer{}
|
||||
for _, area := range clip.Area {
|
||||
areaCTM := NewMatrix(area.CTM)
|
||||
if clips.TransFlag == nil || *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 pathObj.Rule == "Even-Odd" {
|
||||
cp = cp.Settle(canvas.EvenOdd)
|
||||
}
|
||||
renderer.add(cp)
|
||||
}
|
||||
for _, textObj := range area.Text {
|
||||
box, _ := ParseBox(textObj.Boundary)
|
||||
ctm := NewMatrix(textObj.CTM)
|
||||
m := areaCTM.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(ctm)
|
||||
ctx := canvas.NewContext(renderer)
|
||||
ctx.SetView(canvas.Matrix{{m.a, -m.c, bx + m.e}, {-m.b, m.d, pageH - by - m.f}})
|
||||
renderer.clip = r.buildObjectClipPath(textObj.Clips, pageH, box.X, box.Y, ctm, &areaCTM, true)
|
||||
if renderer.clip != nil {
|
||||
renderer.clip = renderer.clip.Translate(bx, -by)
|
||||
}
|
||||
textObj.Boundary, textObj.CTM = "", ""
|
||||
textObj.Clips = nil
|
||||
textObj.Alpha = nil
|
||||
textObj.FillColor = &FillColor{Value: "0 0 0"}
|
||||
textObj.StrokeColor = &StrokeColor{Value: "0 0 0"}
|
||||
textRenderer := *r
|
||||
textRenderer.pageText = nil
|
||||
textRenderer.textOnly = false
|
||||
textRenderer.renderText(ctx, textObj, 0, r.drawParamDefaults(area.DrawParam, nil), nil, false, nil)
|
||||
if textRenderer.renderError != nil {
|
||||
r.renderError = textRenderer.renderError
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if renderer.path == nil {
|
||||
renderer.path = &canvas.Path{}
|
||||
}
|
||||
p = intersectClipPath(p, renderer.path)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// intersectClipPath 求裁剪路径交集
|
||||
// 入参: parent 父级裁剪路径, current 当前裁剪路径
|
||||
// 返回: *canvas.Path 相交后的裁剪路径
|
||||
func intersectClipPath(parent, current *canvas.Path) *canvas.Path {
|
||||
if parent == nil {
|
||||
return current
|
||||
}
|
||||
if current == nil {
|
||||
return parent
|
||||
}
|
||||
parentRect, parentOK := rectangularPath(parent)
|
||||
currentRect, currentOK := rectangularPath(current)
|
||||
if parentOK && currentOK {
|
||||
return parentRect.And(currentRect).ToPath()
|
||||
}
|
||||
if parent.Empty() || current.Empty() {
|
||||
return &canvas.Path{}
|
||||
}
|
||||
if parentOK && parentRect.Contains(current.FastBounds()) {
|
||||
return current
|
||||
}
|
||||
if currentOK && currentRect.Contains(parent.FastBounds()) {
|
||||
return parent
|
||||
}
|
||||
return parent.And(current)
|
||||
}
|
||||
|
||||
// unionClipPath 合并裁剪区域
|
||||
// 入参: left 左侧裁剪路径, right 右侧裁剪路径
|
||||
// 返回: *canvas.Path 合并后的裁剪路径
|
||||
func unionClipPath(left, right *canvas.Path) *canvas.Path {
|
||||
leftRect, leftOK := rectangularPath(left)
|
||||
rightRect, rightOK := rectangularPath(right)
|
||||
if leftOK && rightOK {
|
||||
bounds := leftRect.Add(rightRect)
|
||||
area := leftRect.Area() + rightRect.Area() - leftRect.And(rightRect).Area()
|
||||
if canvas.Equal(area, bounds.Area()) {
|
||||
return bounds.ToPath()
|
||||
}
|
||||
}
|
||||
return left.Or(right)
|
||||
}
|
||||
|
||||
// applyClipPath 应用裁剪路径
|
||||
// 入参: path 绘制路径, clip 裁剪路径
|
||||
// 返回: *canvas.Path 裁剪后的绘制路径
|
||||
func applyClipPath(path, clip *canvas.Path) *canvas.Path {
|
||||
if path == nil || clip == nil {
|
||||
return path
|
||||
}
|
||||
if path.Empty() || clip.Empty() {
|
||||
return &canvas.Path{}
|
||||
}
|
||||
if rect, ok := rectangularPath(clip); ok {
|
||||
bounds := path.FastBounds()
|
||||
if rect.Contains(bounds) {
|
||||
return path
|
||||
}
|
||||
if !rect.Overlaps(bounds) {
|
||||
return &canvas.Path{}
|
||||
}
|
||||
if pathRect, ok := rectangularPath(path); ok {
|
||||
return pathRect.And(rect).ToPath()
|
||||
}
|
||||
}
|
||||
return path.And(clip)
|
||||
}
|
||||
|
||||
// rectangularPath 获取矩形路径区域
|
||||
// 入参: path 路径对象
|
||||
// 返回: canvas.Rect 矩形区域, bool 是否为矩形
|
||||
func rectangularPath(path *canvas.Path) (canvas.Rect, bool) {
|
||||
if path == nil || path.HasSubpaths() || !path.Closed() {
|
||||
return canvas.Rect{}, false
|
||||
}
|
||||
data := path.Data()
|
||||
for i := 0; i < len(data); i += 4 {
|
||||
if i+4 > len(data) || (data[i] != canvas.MoveToCmd && data[i] != canvas.LineToCmd && data[i] != canvas.CloseCmd) {
|
||||
return canvas.Rect{}, false
|
||||
}
|
||||
}
|
||||
points := path.Coords()
|
||||
if len(points) != 5 || !points[0].Equals(points[4]) {
|
||||
return canvas.Rect{}, false
|
||||
}
|
||||
rect := path.FastBounds()
|
||||
corners := 0
|
||||
for _, point := range points[:4] {
|
||||
corner := 0
|
||||
switch {
|
||||
case canvas.Equal(point.X, rect.X0) && canvas.Equal(point.Y, rect.Y0):
|
||||
corner = 1
|
||||
case canvas.Equal(point.X, rect.X1) && canvas.Equal(point.Y, rect.Y0):
|
||||
corner = 2
|
||||
case canvas.Equal(point.X, rect.X1) && canvas.Equal(point.Y, rect.Y1):
|
||||
corner = 4
|
||||
case canvas.Equal(point.X, rect.X0) && canvas.Equal(point.Y, rect.Y1):
|
||||
corner = 8
|
||||
default:
|
||||
return canvas.Rect{}, false
|
||||
}
|
||||
if corners&corner != 0 {
|
||||
return canvas.Rect{}, false
|
||||
}
|
||||
corners |= corner
|
||||
}
|
||||
return rect, corners == 15
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,135 @@
|
||||
// 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"
|
||||
"path"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
"github.com/tdewolff/font"
|
||||
)
|
||||
|
||||
// ResolveFont 按需加载字体资源,保留内嵌数据及外部字体的名称、样式和集合索引匹配
|
||||
// 入参: r 渲染器, id 字体标识, exact 是否禁止无关回退
|
||||
// 返回: ResolvedFont 字体与来源, error 字体不可用错误
|
||||
func (CanvasBackend) ResolveFont(r *Renderer, id string, exact bool) (ResolvedFont, error) {
|
||||
definition := r.Reader.fontCache[id]
|
||||
if definition == nil && !r.Reader.fontResourcesRead && r.Reader.OFD != nil {
|
||||
if _, err := r.Reader.Fonts(); err != nil {
|
||||
return ResolvedFont{}, err
|
||||
}
|
||||
definition = r.Reader.fontCache[id]
|
||||
}
|
||||
if definition == nil {
|
||||
if exact {
|
||||
return ResolvedFont{}, fmt.Errorf("font %q not found", id)
|
||||
}
|
||||
return r.canvasFallbackFont(), nil
|
||||
}
|
||||
if definition.FontFile != "" {
|
||||
data, err := r.Reader.FontData(id)
|
||||
if err != nil {
|
||||
return ResolvedFont{}, err
|
||||
}
|
||||
face, err := r.Reader.embeddedFontFace(*definition)
|
||||
if err != nil {
|
||||
return ResolvedFont{}, err
|
||||
}
|
||||
return ResolvedFont{Data: data, Source: path.Base(definition.FontFile), Face: face, Exact: true}, nil
|
||||
}
|
||||
style := canvasFontStyle(definition)
|
||||
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
|
||||
return ResolvedFont{Data: fontSFNTData(sfnt), Source: source.name, Face: &face, Exact: source.exact}, nil
|
||||
}
|
||||
if !exact {
|
||||
return r.canvasFallbackFont(), nil
|
||||
}
|
||||
return ResolvedFont{}, fmt.Errorf("font %q is unavailable", definition.FontName)
|
||||
}
|
||||
|
||||
// canvasFallbackFont 按需加载默认阅读字体,不用于精确编辑匹配
|
||||
// 返回: ResolvedFont 默认字体,未安装时数据为空
|
||||
func (r *Renderer) canvasFallbackFont() ResolvedFont {
|
||||
if r.fontFamily == nil {
|
||||
r.initCanvasFonts()
|
||||
}
|
||||
if r.defaultFontLoaded {
|
||||
return ResolvedFont{Data: fontSFNTData(r.fontFamily.Face(12, canvas.FontRegular).Font.SFNT)}
|
||||
}
|
||||
return ResolvedFont{}
|
||||
}
|
||||
|
||||
// OpenFont 解析独立SFNT数据,度量与原有编辑排版保持一致
|
||||
// 入参: data 字体数据
|
||||
// 返回: FontMetrics 字体度量, error 解析错误
|
||||
func (CanvasBackend) OpenFont(data []byte) (FontMetrics, error) {
|
||||
sfnt, err := font.ParseSFNT(data, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return canvasFontMetrics{sfnt}, nil
|
||||
}
|
||||
|
||||
// canvasFontMetrics 保留默认字体度量,写出时不更新原始时间
|
||||
type canvasFontMetrics struct {
|
||||
*font.SFNT
|
||||
}
|
||||
|
||||
// Write 返回字体数据,不修改字体元信息
|
||||
// 返回: []byte 独立SFNT数据
|
||||
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()
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
@@ -67,18 +67,22 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
|
||||
if ff, ok := r.fontMap[fontID]; ok {
|
||||
return ff
|
||||
}
|
||||
var defaultFont *canvas.FontFamily
|
||||
if r.defaultFontLoaded {
|
||||
defaultFont = r.fontFamily
|
||||
resolved, err := r.ResolveFont(fontID, false)
|
||||
if err != nil {
|
||||
r.renderError = err
|
||||
return nil
|
||||
}
|
||||
of, ok := r.Reader.fontCache[fontID]
|
||||
if !ok {
|
||||
return defaultFont
|
||||
of := r.Reader.fontCache[fontID]
|
||||
if len(resolved.Data) == 0 {
|
||||
return nil
|
||||
}
|
||||
if of == nil {
|
||||
of = &Font{ID: fontID, FontName: "default"}
|
||||
}
|
||||
fontData := resolved.Data
|
||||
fontStyle := canvasFontStyle(of)
|
||||
ff := canvas.NewFontFamily(of.FontName)
|
||||
if of.FontFile != "" {
|
||||
if fontData, err := r.Reader.FontData(fontID); err == nil {
|
||||
if cidMap := getCFFCIDRuneMap(fontData); len(cidMap) > 0 {
|
||||
r.fontCIDMap[fontID] = cidMap
|
||||
}
|
||||
@@ -99,24 +103,16 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
|
||||
r.fontGIDMap[fontID] = inv
|
||||
}
|
||||
}
|
||||
if err := ff.LoadFont(fontData, 0, fontStyle); err == nil {
|
||||
}
|
||||
if err := ff.LoadFont(fontData, 0, fontStyle); err != nil {
|
||||
r.renderError = err
|
||||
return nil
|
||||
}
|
||||
if r.Reader.fontCache[fontID] != nil {
|
||||
r.fontMap[fontID] = ff
|
||||
}
|
||||
return ff
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for _, source := range r.fontSources(fontID, of, fontStyle) {
|
||||
if loaded := r.loadFontSource(ff, source, fontStyle); loaded != nil {
|
||||
r.fontMap[fontID] = loaded
|
||||
r.fontSourceUsed[fontID] = source
|
||||
return loaded
|
||||
}
|
||||
}
|
||||
r.fontMap[fontID] = defaultFont
|
||||
return defaultFont
|
||||
}
|
||||
|
||||
// canvasFontStyle 获取Canvas字体样式
|
||||
// 入参: font OFD字体定义
|
||||
@@ -213,14 +209,17 @@ func appendFontSource(sources []fontSource, seen map[fontSourceKey]bool, source
|
||||
}
|
||||
|
||||
// fontSourceMatch 获取匹配的字体来源
|
||||
// 入参: fontID 字体ID, font OFD字体定义
|
||||
// 入参: fontID 字体ID, font OFD字体定义, exact 是否禁止无关回退
|
||||
// 返回: fontSource 字体来源, *canvas.FontFamily 已加载的字体族,未匹配时为空
|
||||
func (r *Renderer) fontSourceMatch(fontID string, font *Font) (fontSource, *canvas.FontFamily) {
|
||||
if source, ok := r.fontSourceUsed[fontID]; ok {
|
||||
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]
|
||||
}
|
||||
style := canvasFontStyle(font)
|
||||
for _, source := range r.fontSources(fontID, font, style) {
|
||||
if exact && !source.exact {
|
||||
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 != nil {
|
||||
@@ -0,0 +1,324 @@
|
||||
// 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"
|
||||
"strconv"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
// Path 解析对象的标准紧缩路径并转换到页面坐标
|
||||
// 入参: object 路径对象
|
||||
// 返回: GeometryPath 页面路径, error 无效路径错误
|
||||
func (CanvasBackend) Path(object PathObject) (GeometryPath, error) {
|
||||
if err := creationPath(object.AbbreviatedData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := creationBox(object.Boundary); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if object.CTM != "" {
|
||||
if _, err := creationNumbers(object.CTM, 6); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
p := (&Renderer{}).buildPath(object, 0, NewMatrix(object.CTM), false)
|
||||
return *geometryFromCanvasPath(p), nil
|
||||
}
|
||||
|
||||
// Region 解析动作区域并保持椭圆弧及退化变换的既有处理
|
||||
// 入参: region 动作区域, matrix 页面变换
|
||||
// 返回: GeometryPath 页面路径, error 解析错误
|
||||
func (CanvasBackend) Region(region *Region, matrix Matrix) (GeometryPath, error) {
|
||||
p := actionRegionPath(region, matrix)
|
||||
return *geometryFromCanvasPath(p.Transform(canvas.Matrix{{1, 0, 0}, {0, -1, 0}})), nil
|
||||
}
|
||||
|
||||
// Bounds 计算曲线路径的精确范围
|
||||
// 入参: path 页面路径
|
||||
// 返回: Box 路径范围, error 无效路径错误
|
||||
func (CanvasBackend) Bounds(path GeometryPath) (Box, error) {
|
||||
p, err := geometryToCanvasPath(&path)
|
||||
if err != nil {
|
||||
return Box{}, err
|
||||
}
|
||||
if p.Empty() {
|
||||
return Box{}, nil
|
||||
}
|
||||
box := p.Bounds()
|
||||
return Box{X: box.X0, Y: -box.Y1, W: box.W(), H: box.H()}, nil
|
||||
}
|
||||
|
||||
// Transform 变换路径,保持曲线与椭圆弧,不修改源路径
|
||||
// 入参: path 页面路径, matrix 页面变换
|
||||
// 返回: GeometryPath 新路径, error 无效路径错误
|
||||
func (CanvasBackend) Transform(path GeometryPath, matrix Matrix) (GeometryPath, error) {
|
||||
p, err := geometryToCanvasPath(&path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := matrix.Values()
|
||||
for _, value := range m {
|
||||
if !finite(value) {
|
||||
return nil, fmt.Errorf("invalid geometry transform")
|
||||
}
|
||||
}
|
||||
if geometryEqual(m[0]*m[3]-m[1]*m[2], 0) {
|
||||
p = p.ReplaceArcs()
|
||||
}
|
||||
p = p.Transform(canvas.Matrix{{m[0], -m[2], m[4]}, {-m[1], m[3], -m[5]}})
|
||||
return *geometryFromCanvasPath(p), nil
|
||||
}
|
||||
|
||||
// Normalize 按填充规则整理自交区域
|
||||
// 入参: path 页面路径, evenOdd 是否采用奇偶规则
|
||||
// 返回: GeometryPath 非零填充路径, error 无效路径错误
|
||||
func (CanvasBackend) Normalize(path GeometryPath, evenOdd bool) (GeometryPath, error) {
|
||||
p, err := geometryToCanvasPath(&path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rule := canvas.NonZero
|
||||
if evenOdd {
|
||||
rule = canvas.EvenOdd
|
||||
}
|
||||
p = p.Settle(rule)
|
||||
if p.Empty() {
|
||||
return nil, nil
|
||||
}
|
||||
return *geometryFromCanvasPath(p), nil
|
||||
}
|
||||
|
||||
// Combine 计算两个非零填充区域的布尔运算
|
||||
// 入参: left 左路径, right 右路径, operation 运算类型
|
||||
// 返回: GeometryPath 结果路径, error 运算错误
|
||||
func (CanvasBackend) Combine(left, right GeometryPath, operation GeometryOperation) (GeometryPath, error) {
|
||||
a, err := geometryToCanvasPath(&left)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b, err := geometryToCanvasPath(&right)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result *canvas.Path
|
||||
switch operation {
|
||||
case GeometryIntersect:
|
||||
result = a.And(b)
|
||||
case GeometryUnion:
|
||||
result = a.Or(b)
|
||||
case GeometrySubtract:
|
||||
result = a.Not(b)
|
||||
case GeometryXor:
|
||||
result = a.Xor(b)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported geometry operation %d", operation)
|
||||
}
|
||||
if result.Empty() {
|
||||
return nil, nil
|
||||
}
|
||||
return *geometryFromCanvasPath(result), nil
|
||||
}
|
||||
|
||||
// Stroke 将绝对长度虚线与描边转换为填充区域
|
||||
// 入参: path 页面路径, options 描边样式
|
||||
// 返回: GeometryPath 描边区域, error 样式或路径错误
|
||||
func (CanvasBackend) Stroke(path GeometryPath, options StrokeOptions) (GeometryPath, error) {
|
||||
if !finite(options.Width) || options.Width <= 0 || !finite(options.Tolerance) || options.Tolerance < 0 || !finite(options.DashOffset) || !finite(options.MiterLimit) || options.MiterLimit < 0 {
|
||||
return nil, fmt.Errorf("invalid stroke width or tolerance")
|
||||
}
|
||||
if options.Cap != "" && options.Cap != "Butt" && options.Cap != "Round" && options.Cap != "Square" {
|
||||
return nil, fmt.Errorf("invalid stroke cap %q", options.Cap)
|
||||
}
|
||||
if options.Join != "" && options.Join != "Miter" && options.Join != "Round" && options.Join != "Bevel" {
|
||||
return nil, fmt.Errorf("invalid stroke join %q", options.Join)
|
||||
}
|
||||
for _, dash := range options.Dashes {
|
||||
if !finite(dash) || dash < 0 {
|
||||
return nil, fmt.Errorf("invalid stroke dash")
|
||||
}
|
||||
}
|
||||
p, err := geometryToCanvasPath(&path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tolerance := options.Tolerance
|
||||
if tolerance == 0 {
|
||||
tolerance = canvas.Tolerance
|
||||
}
|
||||
style := pathStyle{lineJoin: canvas.MiterJoin, lineCap: canvas.ButtCap, miterLimit: defaultMiterLimit}
|
||||
style.applyLineJoin(options.Join, options.MiterLimit)
|
||||
style.lineCap = pathLineCap(options.Cap, style.lineCap)
|
||||
p = p.Dash(options.DashOffset, options.Dashes...).Stroke(options.Width, style.lineCap, style.lineJoin, tolerance)
|
||||
return *geometryFromCanvasPath(p), nil
|
||||
}
|
||||
|
||||
// Clip 解析对象裁剪并与父裁剪相交,保留未裁剪与完全裁去的区别
|
||||
// 入参: r 渲染器, clips OFD裁剪, matrix 页面变换, parent 父裁剪
|
||||
// 返回: *GeometryPath 页面裁剪, error 无效父路径错误
|
||||
func (CanvasBackend) Clip(r *Renderer, clips *Clips, matrix Matrix, parent *GeometryPath) (*GeometryPath, error) {
|
||||
p, err := geometryToCanvasPath(parent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
renderer := *r
|
||||
renderer.renderError = nil
|
||||
clip := renderer.buildClipPath(clips, 0, 0, 0, matrix)
|
||||
if renderer.renderError != nil {
|
||||
return nil, renderer.renderError
|
||||
}
|
||||
return geometryFromCanvasPath(intersectClipPath(p, clip)), nil
|
||||
}
|
||||
|
||||
// geometryToCanvasPath 将页面坐标路径转换为默认几何引擎的向上纵轴
|
||||
// 入参: path 独立几何路径,nil表示不裁剪
|
||||
// 返回: *canvas.Path 默认引擎路径, error 不支持的路径指令
|
||||
func geometryToCanvasPath(path *GeometryPath) (*canvas.Path, error) {
|
||||
if path == nil {
|
||||
return nil, nil
|
||||
}
|
||||
p := &canvas.Path{}
|
||||
for _, segment := range *path {
|
||||
switch segment.Verb {
|
||||
case GeometryMove:
|
||||
p.MoveTo(segment.End.X, -segment.End.Y)
|
||||
case GeometryLine:
|
||||
p.LineTo(segment.End.X, -segment.End.Y)
|
||||
case GeometryQuad:
|
||||
p.QuadTo(segment.Control1.X, -segment.Control1.Y, segment.End.X, -segment.End.Y)
|
||||
case GeometryCubic:
|
||||
p.CubeTo(segment.Control1.X, -segment.Control1.Y, segment.Control2.X, -segment.Control2.Y, segment.End.X, -segment.End.Y)
|
||||
case GeometryArc:
|
||||
p.ArcTo(segment.RadiusX, segment.RadiusY, -segment.Rotation, segment.Large, !segment.Sweep, segment.End.X, -segment.End.Y)
|
||||
case GeometryClose:
|
||||
p.Close()
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported geometry command %d", segment.Verb)
|
||||
}
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// geometryFromCanvasPath 将默认引擎路径转换为独立的页面坐标路径,保留曲线和椭圆弧
|
||||
// 入参: path 默认引擎路径
|
||||
// 返回: *GeometryPath 独立路径,nil表示不裁剪
|
||||
func geometryFromCanvasPath(path *canvas.Path) *GeometryPath {
|
||||
if path == nil {
|
||||
return nil
|
||||
}
|
||||
result := make(GeometryPath, 0, path.Len())
|
||||
scanner := path.Scanner()
|
||||
for scanner.Scan() {
|
||||
end := scanner.End()
|
||||
segment := GeometrySegment{End: Point{X: end.X, Y: -end.Y}}
|
||||
switch scanner.Cmd() {
|
||||
case canvas.MoveToCmd:
|
||||
segment.Verb = GeometryMove
|
||||
case canvas.LineToCmd:
|
||||
segment.Verb = GeometryLine
|
||||
case canvas.QuadToCmd:
|
||||
control := scanner.CP1()
|
||||
segment.Verb, segment.Control1 = GeometryQuad, Point{X: control.X, Y: -control.Y}
|
||||
case canvas.CubeToCmd:
|
||||
c1, c2 := scanner.CP1(), scanner.CP2()
|
||||
segment.Verb = GeometryCubic
|
||||
segment.Control1, segment.Control2 = Point{X: c1.X, Y: -c1.Y}, Point{X: c2.X, Y: -c2.Y}
|
||||
case canvas.ArcToCmd:
|
||||
segment.Verb = GeometryArc
|
||||
segment.RadiusX, segment.RadiusY, segment.Rotation, segment.Large, segment.Sweep = scanner.Arc()
|
||||
segment.Rotation, segment.Sweep = -segment.Rotation, !segment.Sweep
|
||||
case canvas.CloseCmd:
|
||||
segment.Verb = GeometryClose
|
||||
}
|
||||
result = append(result, segment)
|
||||
}
|
||||
return &result
|
||||
}
|
||||
|
||||
// actionCanvasMatrix 转换页面坐标矩阵,不翻转Y轴
|
||||
// 入参: matrix 页面变换
|
||||
// 返回: canvas.Matrix 路径变换
|
||||
func actionCanvasMatrix(matrix Matrix) canvas.Matrix {
|
||||
return canvas.Matrix{{matrix.a, matrix.c, matrix.e}, {matrix.b, matrix.d, matrix.f}}
|
||||
}
|
||||
|
||||
// actionRegionPath 解析复杂点击区域并转换为页面坐标
|
||||
// 入参: region 动作区域, matrix 坐标变换
|
||||
// 返回: *canvas.Path 点击路径
|
||||
func actionRegionPath(region *Region, matrix Matrix) *canvas.Path {
|
||||
path := &canvas.Path{}
|
||||
for _, area := range region.Area {
|
||||
start := parseFloats(area.Start)
|
||||
if len(start) != 2 {
|
||||
continue
|
||||
}
|
||||
part := &canvas.Path{}
|
||||
part.MoveTo(start[0], start[1])
|
||||
for _, command := range area.Command {
|
||||
p1, p2, p3 := parseFloats(command.Point1), parseFloats(command.Point2), parseFloats(command.Point3)
|
||||
switch command.Type {
|
||||
case "Move":
|
||||
if len(p1) == 2 {
|
||||
part.Close()
|
||||
part.MoveTo(p1[0], p1[1])
|
||||
}
|
||||
case "Line":
|
||||
if len(p1) == 2 {
|
||||
part.LineTo(p1[0], p1[1])
|
||||
}
|
||||
case "QuadraticBezier":
|
||||
if len(p1) == 2 && len(p2) == 2 {
|
||||
part.QuadTo(p1[0], p1[1], p2[0], p2[1])
|
||||
}
|
||||
case "CubicBezier":
|
||||
if len(p3) == 2 {
|
||||
first, second := part.Pos(), canvas.Point{X: p3[0], Y: p3[1]}
|
||||
if len(p1) == 2 {
|
||||
first = canvas.Point{X: p1[0], Y: p1[1]}
|
||||
}
|
||||
if len(p2) == 2 {
|
||||
second = canvas.Point{X: p2[0], Y: p2[1]}
|
||||
}
|
||||
part.CubeTo(first.X, first.Y, second.X, second.Y, p3[0], p3[1])
|
||||
}
|
||||
case "Arc":
|
||||
size, end := parseFloats(command.EllipseSize), parseFloats(command.EndPoint)
|
||||
angle, err := strconv.ParseFloat(command.RotationAngle, 64)
|
||||
large, largeErr := strconv.ParseBool(command.LargeArc)
|
||||
sweep, sweepErr := strconv.ParseBool(command.SweepDirection)
|
||||
if len(end) == 2 && err == nil && largeErr == nil && sweepErr == nil {
|
||||
rx, ry := 0.0, 0.0
|
||||
if len(size) > 0 {
|
||||
rx, ry = size[0], size[0]
|
||||
}
|
||||
if len(size) > 1 {
|
||||
ry = size[1]
|
||||
}
|
||||
part.ArcTo(rx, ry, angle, large, sweep, end[0], end[1])
|
||||
}
|
||||
case "Close":
|
||||
part.Close()
|
||||
}
|
||||
}
|
||||
part.Close()
|
||||
path = path.Append(part)
|
||||
}
|
||||
if canvas.Equal(matrix.a*matrix.d-matrix.b*matrix.c, 0) {
|
||||
path = path.ReplaceArcs()
|
||||
}
|
||||
return path.Transform(actionCanvasMatrix(matrix))
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,111 @@
|
||||
// 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 (
|
||||
"github.com/tdewolff/canvas"
|
||||
canvastext "github.com/tdewolff/canvas/text"
|
||||
"github.com/tdewolff/font"
|
||||
)
|
||||
|
||||
// textGlyphPathCacheKey 字形路径缓存键
|
||||
type textGlyphPathCacheKey struct {
|
||||
font *canvas.Font
|
||||
size float64
|
||||
fauxBold float64
|
||||
fauxItalic float64
|
||||
xOffset int32
|
||||
yOffset int32
|
||||
language string
|
||||
script canvastext.Script
|
||||
direction canvastext.Direction
|
||||
glyph textGlyph
|
||||
}
|
||||
|
||||
// textGlyphPathCacheValue 字形路径缓存值
|
||||
type textGlyphPathCacheValue struct {
|
||||
path *canvas.Path
|
||||
width float64
|
||||
}
|
||||
|
||||
// textGlyphWidth 获取绘制字形宽度
|
||||
// 入参: face 字体, glyph 绘制字形
|
||||
// 返回: float64 字形宽度
|
||||
func textGlyphWidth(face *canvas.FontFace, glyph textGlyph) float64 {
|
||||
if glyph.GlyphID >= 0 && glyph.GlyphID <= 0xFFFF {
|
||||
return face.MmPerEm * float64(face.Font.GlyphAdvance(uint16(glyph.GlyphID)))
|
||||
}
|
||||
return face.TextWidth(glyph.Text)
|
||||
}
|
||||
|
||||
// textGlyphPath 获取绘制字形路径
|
||||
// 入参: face 字体, glyph 绘制字形
|
||||
// 返回: *canvas.Path 字形路径, float64 字形宽度
|
||||
func textGlyphPath(face *canvas.FontFace, glyph textGlyph) (*canvas.Path, float64) {
|
||||
if glyph.GlyphID < 0 || glyph.GlyphID > 0xFFFF {
|
||||
return face.ToPath(glyph.Text)
|
||||
}
|
||||
p := &canvas.Path{}
|
||||
glyphID := uint16(glyph.GlyphID)
|
||||
_ = face.Font.GlyphPath(p, glyphID, face.PPEM(canvas.DefaultResolution), 0, 0, face.MmPerEm, font.NoHinting)
|
||||
if face.FauxBold != 0 {
|
||||
d := face.FauxBold * face.Size
|
||||
if face.Font.IsTrueType {
|
||||
d = -d
|
||||
}
|
||||
origFastStroke := canvas.FastStroke
|
||||
canvas.FastStroke = true
|
||||
p = p.Offset(d, canvas.Tolerance)
|
||||
canvas.FastStroke = origFastStroke
|
||||
}
|
||||
if face.FauxItalic != 0 {
|
||||
p = p.Transform(canvas.Identity.Shear(face.FauxItalic, 0))
|
||||
}
|
||||
return p, face.MmPerEm * float64(face.Font.GlyphAdvance(glyphID))
|
||||
}
|
||||
|
||||
// cachedTextGlyphPath 获取缓存的字形路径
|
||||
// 入参: face 字体, glyph 绘制字形
|
||||
// 返回: *canvas.Path 字形路径, float64 字形宽度
|
||||
func (r *Renderer) cachedTextGlyphPath(face *canvas.FontFace, glyph textGlyph) (*canvas.Path, float64) {
|
||||
key := textGlyphPathCacheKey{
|
||||
font: face.Font,
|
||||
size: face.Size,
|
||||
fauxBold: face.FauxBold,
|
||||
fauxItalic: face.FauxItalic,
|
||||
xOffset: face.XOffset,
|
||||
yOffset: face.YOffset,
|
||||
language: face.Language,
|
||||
script: face.Script,
|
||||
direction: face.Direction,
|
||||
glyph: glyph,
|
||||
}
|
||||
if cached, ok := r.textGlyphPathCache[key]; ok {
|
||||
return cached.path, cached.width
|
||||
}
|
||||
path, width := textGlyphPath(face, glyph)
|
||||
r.textGlyphPathCache[key] = textGlyphPathCacheValue{path: path, width: width}
|
||||
return path, width
|
||||
}
|
||||
|
||||
// textMatrix 获取文本字形变换矩阵
|
||||
// 入参: ctm OFD变换矩阵
|
||||
// 返回: canvas.Matrix 画布变换矩阵
|
||||
func textMatrix(ctm Matrix) canvas.Matrix {
|
||||
return canvas.Matrix{
|
||||
{ctm.a, -ctm.c, 0},
|
||||
{-ctm.b, ctm.d, 0},
|
||||
}
|
||||
}
|
||||
@@ -18,13 +18,11 @@ import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
"math"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
canvasimage "github.com/tdewolff/canvas/image"
|
||||
"github.com/tdewolff/canvas/renderers/rasterizer"
|
||||
"golang.org/x/image/draw"
|
||||
)
|
||||
|
||||
@@ -84,7 +82,7 @@ func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float
|
||||
}
|
||||
}
|
||||
clipPath := intersectClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, box.X, box.Y, localCTM, parentCTM, boundaryInCTM))
|
||||
img = imageWithClip(img, clipPath, m, 96)
|
||||
img = r.imageWithClip(img, clipPath, m, 96)
|
||||
m = m.Scale(imgW/float64(img.Bounds().Dx()), imgH/float64(img.Bounds().Dy()))
|
||||
img, pad := imageWithTransparentEdge(img)
|
||||
if pad > 0 {
|
||||
@@ -127,58 +125,11 @@ func (r *Renderer) renderImageBorder(ctx *canvas.Context, obj ImageObject, box B
|
||||
}, pageH, nil, parentCTM, boundaryInCTM, clipPath)
|
||||
}
|
||||
|
||||
// imageWithMask 应用图片蒙版
|
||||
// 入参: img 图片对象, mask 蒙版图片
|
||||
// 返回: image.Image 蒙版处理后的图片对象
|
||||
func imageWithMask(img, mask image.Image) image.Image {
|
||||
if img == nil || mask == nil {
|
||||
return img
|
||||
}
|
||||
bounds := img.Bounds()
|
||||
if bounds.Empty() || mask.Bounds().Empty() {
|
||||
return img
|
||||
}
|
||||
source := imagePixelSource(img)
|
||||
maskSource := imagePixelSource(mask)
|
||||
maskBounds := maskSource.Bounds()
|
||||
var opacity image.Image = maskSource
|
||||
if bounds.Size() != maskBounds.Size() {
|
||||
resized := image.NewGray(bounds)
|
||||
draw.CatmullRom.Scale(resized, bounds, maskSource, maskBounds, draw.Src, nil)
|
||||
opacity = resized
|
||||
maskBounds = bounds
|
||||
}
|
||||
out := image.NewNRGBA(bounds)
|
||||
if src, ok := source.(*image.NRGBA); ok {
|
||||
for y := 0; y < bounds.Dy(); y++ {
|
||||
offset := src.PixOffset(bounds.Min.X, bounds.Min.Y+y)
|
||||
row := out.Pix[y*out.Stride : (y+1)*out.Stride]
|
||||
copy(row, src.Pix[offset:offset+len(row)])
|
||||
for x := 3; x < len(row); x += 4 {
|
||||
a := imageGrayAt(opacity, maskBounds.Min.X+x/4, maskBounds.Min.Y+y).Y
|
||||
row[x] = uint8(int(row[x]) * int(a) / 255)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
mx := maskBounds.Min.X + x - bounds.Min.X
|
||||
my := maskBounds.Min.Y + y - bounds.Min.Y
|
||||
a := imageGrayAt(opacity, mx, my).Y
|
||||
c := imageNRGBAAt(source, x, y)
|
||||
c.A = uint8(int(c.A) * int(a) / 255)
|
||||
out.SetNRGBA(x, y, c)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// imageWithClip 应用图片裁剪区域
|
||||
// 低分辨率图片按最低精度细化蒙版,细化后的像素上限16Mi,不降低原图分辨率
|
||||
// 入参: img 图片对象, clipPath 裁剪路径, m 图片变换矩阵, dpi 最低蒙版分辨率,0保留原图精度
|
||||
// 返回: image.Image 裁剪后的图片对象
|
||||
func imageWithClip(img image.Image, clipPath *canvas.Path, m canvas.Matrix, dpi float64) image.Image {
|
||||
func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canvas.Matrix, dpi float64) image.Image {
|
||||
if img == nil || clipPath == nil || m.Det() == 0 {
|
||||
return img
|
||||
}
|
||||
@@ -215,12 +166,29 @@ func imageWithClip(img image.Image, clipPath *canvas.Path, m canvas.Matrix, dpi
|
||||
bounds, w, h = resized.Bounds(), nw, nh
|
||||
}
|
||||
clip := clipPath.Copy().Transform(m.Inv())
|
||||
clipCanvas := canvas.New(float64(w), float64(h))
|
||||
ctx := canvas.NewContext(clipCanvas)
|
||||
compiler := &canvasPageCompiler{page: &RasterPage{Width: float64(w), Height: float64(h), DPI: 25.4}}
|
||||
ctx := canvas.NewContext(compiler)
|
||||
ctx.SetFillColor(canvas.White)
|
||||
ctx.SetStrokeColor(canvas.Transparent)
|
||||
ctx.DrawPath(0, 0, clip)
|
||||
mask := rasterizer.Draw(clipCanvas, canvas.DPMM(1), canvas.DefaultColorSpace)
|
||||
if compiler.err != nil {
|
||||
r.renderError = compiler.err
|
||||
return img
|
||||
}
|
||||
if r.backends.Raster == nil {
|
||||
r.renderError = fmt.Errorf("image clip raster: %w", ErrBackendUnavailable)
|
||||
return img
|
||||
}
|
||||
result, err := r.backends.Raster.Render(compiler.page)
|
||||
if err != nil {
|
||||
r.renderError = err
|
||||
return img
|
||||
}
|
||||
mask, ok := result.(*image.RGBA)
|
||||
if !ok {
|
||||
mask = image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
draw.Draw(mask, mask.Bounds(), result, result.Bounds().Min, draw.Src)
|
||||
}
|
||||
if mask.Opaque() {
|
||||
return img
|
||||
}
|
||||
@@ -306,213 +274,3 @@ func (r *Renderer) readImageResource(resPath string) (image.Image, error) {
|
||||
img, _, err := decodeImageData(data)
|
||||
return img, err
|
||||
}
|
||||
|
||||
// imageWithAlpha 合并图片透明度
|
||||
// 入参: img 图片对象, alpha 对象透明度
|
||||
// 返回: image.Image 合并后的图片对象
|
||||
func imageWithAlpha(img image.Image, alpha *int) image.Image {
|
||||
if img == nil || alpha == nil {
|
||||
return img
|
||||
}
|
||||
a := clampColor(*alpha)
|
||||
if a == 255 {
|
||||
return img
|
||||
}
|
||||
bounds := img.Bounds()
|
||||
out := image.NewNRGBA(bounds)
|
||||
source := imagePixelSource(img)
|
||||
if src, ok := source.(*image.NRGBA); ok {
|
||||
for y := 0; y < bounds.Dy(); y++ {
|
||||
offset := src.PixOffset(bounds.Min.X, bounds.Min.Y+y)
|
||||
row := out.Pix[y*out.Stride : (y+1)*out.Stride]
|
||||
copy(row, src.Pix[offset:offset+len(row)])
|
||||
for x := 3; x < len(row); x += 4 {
|
||||
row[x] = uint8(int(row[x]) * a / 255)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
c := imageNRGBAAt(source, x, y)
|
||||
c.A = uint8(int(c.A) * a / 255)
|
||||
out.SetNRGBA(x, y, c)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// imageWithTransparentEdge 补齐透明图片边缘颜色
|
||||
// 入参: img 图片对象
|
||||
// 返回: image.Image 补齐后的图片对象, int 补齐像素数
|
||||
func imageWithTransparentEdge(img image.Image) (image.Image, int) {
|
||||
if img == nil {
|
||||
return img, 0
|
||||
}
|
||||
bounds := img.Bounds()
|
||||
w, h := bounds.Dx(), bounds.Dy()
|
||||
if w == 0 || h == 0 {
|
||||
return img, 0
|
||||
}
|
||||
if src, ok := img.(*canvasimage.Image); ok && src.Mimetype == "image/jpeg" && src.Mask == nil {
|
||||
return img, 0
|
||||
}
|
||||
source := imagePixelSource(img)
|
||||
if opaque, ok := source.(interface{ Opaque() bool }); ok && opaque.Opaque() {
|
||||
return img, 0
|
||||
}
|
||||
hasZero, hasVisible := false, false
|
||||
src, ok := source.(*image.NRGBA)
|
||||
if ok {
|
||||
srcBounds := src.Bounds()
|
||||
hasZero = src.Pix[3] == 0
|
||||
hasVisible = !hasZero
|
||||
scan:
|
||||
for y := srcBounds.Min.Y; y < srcBounds.Max.Y; y++ {
|
||||
offset := src.PixOffset(srcBounds.Min.X, y) + 3
|
||||
for x := 0; x < w; x++ {
|
||||
if (src.Pix[offset] == 0) != hasZero {
|
||||
hasZero, hasVisible = true, true
|
||||
break scan
|
||||
}
|
||||
offset += 4
|
||||
}
|
||||
}
|
||||
} else {
|
||||
src = image.NewNRGBA(image.Rect(0, 0, w, h))
|
||||
for y := 0; y < h; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
c := imageNRGBAAt(source, bounds.Min.X+x, bounds.Min.Y+y)
|
||||
src.SetNRGBA(x, y, c)
|
||||
if c.A == 0 {
|
||||
hasZero = true
|
||||
} else {
|
||||
hasVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hasZero || !hasVisible {
|
||||
return img, 0
|
||||
}
|
||||
srcBounds := src.Bounds()
|
||||
out := image.NewNRGBA(image.Rect(0, 0, w+2, h+2))
|
||||
draw.Draw(out, out.Bounds().Inset(1), src, srcBounds.Min, draw.Src)
|
||||
for y := 0; y < h; y++ {
|
||||
sy := srcBounds.Min.Y + y
|
||||
offset := src.PixOffset(srcBounds.Min.X, sy) + 3
|
||||
for x := 0; x < w; x++ {
|
||||
if src.Pix[offset] == 0 {
|
||||
if edge, ok := transparentEdgeColor(src, srcBounds.Min.X+x, sy); ok {
|
||||
out.SetNRGBA(x+1, y+1, edge)
|
||||
}
|
||||
}
|
||||
offset += 4
|
||||
}
|
||||
}
|
||||
for x := 0; x < w; x++ {
|
||||
out.SetNRGBA(x+1, 0, transparentPaddingColor(out.NRGBAAt(x+1, 1)))
|
||||
out.SetNRGBA(x+1, h+1, transparentPaddingColor(out.NRGBAAt(x+1, h)))
|
||||
}
|
||||
for y := 0; y < h; y++ {
|
||||
out.SetNRGBA(0, y+1, transparentPaddingColor(out.NRGBAAt(1, y+1)))
|
||||
out.SetNRGBA(w+1, y+1, transparentPaddingColor(out.NRGBAAt(w, y+1)))
|
||||
}
|
||||
out.SetNRGBA(0, 0, transparentPaddingColor(out.NRGBAAt(1, 1)))
|
||||
out.SetNRGBA(w+1, 0, transparentPaddingColor(out.NRGBAAt(w, 1)))
|
||||
out.SetNRGBA(0, h+1, transparentPaddingColor(out.NRGBAAt(1, h)))
|
||||
out.SetNRGBA(w+1, h+1, transparentPaddingColor(out.NRGBAAt(w, h)))
|
||||
return out, 1
|
||||
}
|
||||
|
||||
// imagePixelSource 获取图片像素源
|
||||
// 入参: img 图片对象
|
||||
// 返回: image.Image 图片像素源
|
||||
func imagePixelSource(img image.Image) image.Image {
|
||||
if src, ok := img.(*canvasimage.Image); ok {
|
||||
if decoded, err := src.Image(); err == nil {
|
||||
return decoded
|
||||
}
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
// imageGrayAt 获取图片灰度像素
|
||||
// 入参: img 图片对象, x X坐标, y Y坐标
|
||||
// 返回: color.Gray 灰度像素
|
||||
func imageGrayAt(img image.Image, x, y int) color.Gray {
|
||||
if src, ok := img.(*image.Gray); ok {
|
||||
return src.GrayAt(x, y)
|
||||
}
|
||||
if src, ok := img.(*image.Paletted); ok {
|
||||
return color.GrayModel.Convert(src.At(x, y)).(color.Gray)
|
||||
}
|
||||
if src, ok := img.(image.RGBA64Image); ok {
|
||||
r, g, b, _ := src.RGBA64At(x, y).RGBA()
|
||||
return color.Gray{Y: uint8((19595*r + 38470*g + 7471*b + 1<<15) >> 24)}
|
||||
}
|
||||
return color.GrayModel.Convert(img.At(x, y)).(color.Gray)
|
||||
}
|
||||
|
||||
// imageNRGBAAt 获取图片NRGBA像素
|
||||
// 入参: img 图片对象, x X坐标, y Y坐标
|
||||
// 返回: color.NRGBA NRGBA像素
|
||||
func imageNRGBAAt(img image.Image, x, y int) color.NRGBA {
|
||||
if src, ok := img.(*image.NRGBA); ok {
|
||||
return src.NRGBAAt(x, y)
|
||||
}
|
||||
if src, ok := img.(*image.YCbCr); ok {
|
||||
r, g, b, _ := src.YCbCrAt(x, y).RGBA()
|
||||
return color.NRGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: 255}
|
||||
}
|
||||
if src, ok := img.(*image.RGBA); ok {
|
||||
c := src.RGBA64At(x, y)
|
||||
if c.A == 0 {
|
||||
return color.NRGBA{}
|
||||
}
|
||||
r, g, b, a := uint32(c.R), uint32(c.G), uint32(c.B), uint32(c.A)
|
||||
if a != 0xffff {
|
||||
r = r * 0xffff / a
|
||||
g = g * 0xffff / a
|
||||
b = b * 0xffff / a
|
||||
}
|
||||
return color.NRGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)}
|
||||
}
|
||||
return color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA)
|
||||
}
|
||||
|
||||
// transparentEdgeColor 获取透明像素相邻的可见颜色
|
||||
// 入参: img 图片对象, x X坐标, y Y坐标
|
||||
// 返回: color.NRGBA 颜色, bool 是否存在
|
||||
func transparentEdgeColor(img *image.NRGBA, x, y int) (color.NRGBA, bool) {
|
||||
bounds := img.Bounds()
|
||||
var best color.NRGBA
|
||||
for dy := -1; dy <= 1; dy++ {
|
||||
for dx := -1; dx <= 1; dx++ {
|
||||
if dx == 0 && dy == 0 {
|
||||
continue
|
||||
}
|
||||
nx, ny := x+dx, y+dy
|
||||
if nx < bounds.Min.X || nx >= bounds.Max.X || ny < bounds.Min.Y || ny >= bounds.Max.Y {
|
||||
continue
|
||||
}
|
||||
c := img.NRGBAAt(nx, ny)
|
||||
if c.A > best.A {
|
||||
best = c
|
||||
}
|
||||
}
|
||||
}
|
||||
if best.A == 0 {
|
||||
return color.NRGBA{}, false
|
||||
}
|
||||
best.A = 1
|
||||
return best, true
|
||||
}
|
||||
|
||||
// transparentPaddingColor 获取透明补齐颜色
|
||||
// 入参: c 边缘颜色
|
||||
// 返回: color.NRGBA 补齐颜色
|
||||
func transparentPaddingColor(c color.NRGBA) color.NRGBA {
|
||||
c.A = 1
|
||||
return c
|
||||
}
|
||||
@@ -51,40 +51,6 @@ func (r *Renderer) renderTemplate(ctx *canvas.Context, templateID string, pageH
|
||||
}
|
||||
}
|
||||
|
||||
// loadTemplate 加载模板页面并复用解析结果
|
||||
// 入参: templateID 模板ID
|
||||
// 返回: *PageContent 模板页面
|
||||
func (r *Renderer) loadTemplate(templateID string) *PageContent {
|
||||
if page := r.templatePageCache[templateID]; page != nil {
|
||||
return page
|
||||
}
|
||||
for _, tpl := range r.Reader.doc.CommonData.TemplatePage {
|
||||
if tpl.ID != templateID {
|
||||
continue
|
||||
}
|
||||
page, err := r.Reader.PageContent(Page{BaseLoc: tpl.BaseLoc})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
r.templatePageCache[templateID] = page
|
||||
return page
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// layerOrder 获取图层类型的绘制顺序
|
||||
// 入参: kind 图层类型
|
||||
// 返回: int 绘制顺序
|
||||
func layerOrder(kind string) int {
|
||||
switch kind {
|
||||
case "Background":
|
||||
return 0
|
||||
case "Foreground":
|
||||
return 2
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// renderLayers 按原顺序渲染指定类型的图层
|
||||
// 入参: ctx 画布上下文, layers 图层列表, pageH 页面高度, order 图层类型顺序
|
||||
func (r *Renderer) renderLayers(ctx *canvas.Context, layers []Layer, pageH float64, order int) {
|
||||
@@ -206,108 +172,3 @@ func (r *Renderer) renderObject(ctx *canvas.Context, obj *GraphicObject, pageH f
|
||||
r.renderCompositeGraphicUnit(ctx, obj.CompositeGraphicUnit, pageH, defaults, parentCTM, boundaryInCTM, parentClip)
|
||||
}
|
||||
}
|
||||
|
||||
// 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, defaults 默认绘制参数
|
||||
// 返回: *DrawParam 合并后的绘制参数
|
||||
func (r *Renderer) drawParamDefaults(id string, defaults *DrawParam) *DrawParam {
|
||||
if id == "" {
|
||||
return defaults
|
||||
}
|
||||
dp := r.getDrawParam(id, nil)
|
||||
if dp == nil {
|
||||
return defaults
|
||||
}
|
||||
if defaults == nil {
|
||||
return dp
|
||||
}
|
||||
return mergeDrawParam(*defaults, dp)
|
||||
}
|
||||
|
||||
// mergeDrawParam 合并绘制参数属性
|
||||
// 入参: base 基础绘制参数, dp 覆盖绘制参数
|
||||
// 返回: *DrawParam 合并后的绘制参数
|
||||
func mergeDrawParam(base DrawParam, dp *DrawParam) *DrawParam {
|
||||
if dp.LineWidth > 0 {
|
||||
base.LineWidth = dp.LineWidth
|
||||
}
|
||||
if dp.Join != "" {
|
||||
base.Join = dp.Join
|
||||
}
|
||||
if dp.Cap != "" {
|
||||
base.Cap = dp.Cap
|
||||
}
|
||||
if dp.DashPattern != "" || dp.dashPatternSet {
|
||||
base.DashPattern = dp.DashPattern
|
||||
base.dashPatternSet = dp.dashPatternSet
|
||||
}
|
||||
if dp.DashOffset != nil {
|
||||
base.DashOffset = dp.DashOffset
|
||||
}
|
||||
if dp.MiterLimit > 0 {
|
||||
base.MiterLimit = dp.MiterLimit
|
||||
}
|
||||
if dp.FillColor != nil {
|
||||
base.FillColor = dp.FillColor
|
||||
}
|
||||
if dp.StrokeColor != nil {
|
||||
base.StrokeColor = dp.StrokeColor
|
||||
}
|
||||
if dp.Font != "" {
|
||||
base.Font = dp.Font
|
||||
}
|
||||
if dp.Size > 0 {
|
||||
base.Size = dp.Size
|
||||
}
|
||||
if dp.Weight > 0 {
|
||||
base.Weight = dp.Weight
|
||||
}
|
||||
if dp.Italic {
|
||||
base.Italic = dp.Italic
|
||||
}
|
||||
return &base
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
return mergeDrawParam(*base, dp)
|
||||
}
|
||||
return dp
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -21,71 +21,6 @@ import (
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
// canvasGeometryPath 将页面坐标路径转换为默认几何引擎的向上纵轴
|
||||
// 入参: path 独立几何路径,nil表示不裁剪
|
||||
// 返回: *canvas.Path 默认引擎路径, error 不支持的路径指令
|
||||
func canvasGeometryPath(path *GeometryPath) (*canvas.Path, error) {
|
||||
if path == nil {
|
||||
return nil, nil
|
||||
}
|
||||
p := &canvas.Path{}
|
||||
for _, segment := range *path {
|
||||
switch segment.Verb {
|
||||
case GeometryMove:
|
||||
p.MoveTo(segment.End.X, -segment.End.Y)
|
||||
case GeometryLine:
|
||||
p.LineTo(segment.End.X, -segment.End.Y)
|
||||
case GeometryQuad:
|
||||
p.QuadTo(segment.Control1.X, -segment.Control1.Y, segment.End.X, -segment.End.Y)
|
||||
case GeometryCubic:
|
||||
p.CubeTo(segment.Control1.X, -segment.Control1.Y, segment.Control2.X, -segment.Control2.Y, segment.End.X, -segment.End.Y)
|
||||
case GeometryArc:
|
||||
p.ArcTo(segment.RadiusX, segment.RadiusY, -segment.Rotation, segment.Large, !segment.Sweep, segment.End.X, -segment.End.Y)
|
||||
case GeometryClose:
|
||||
p.Close()
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported geometry command %d", segment.Verb)
|
||||
}
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// geometryCanvasPath 将默认引擎路径转换为独立的页面坐标路径,保留曲线和椭圆弧
|
||||
// 入参: path 默认引擎路径
|
||||
// 返回: *GeometryPath 独立路径,nil表示不裁剪
|
||||
func geometryCanvasPath(path *canvas.Path) *GeometryPath {
|
||||
if path == nil {
|
||||
return nil
|
||||
}
|
||||
result := make(GeometryPath, 0, path.Len())
|
||||
scanner := path.Scanner()
|
||||
for scanner.Scan() {
|
||||
end := scanner.End()
|
||||
segment := GeometrySegment{End: Point{X: end.X, Y: -end.Y}}
|
||||
switch scanner.Cmd() {
|
||||
case canvas.MoveToCmd:
|
||||
segment.Verb = GeometryMove
|
||||
case canvas.LineToCmd:
|
||||
segment.Verb = GeometryLine
|
||||
case canvas.QuadToCmd:
|
||||
control := scanner.CP1()
|
||||
segment.Verb, segment.Control1 = GeometryQuad, Point{X: control.X, Y: -control.Y}
|
||||
case canvas.CubeToCmd:
|
||||
c1, c2 := scanner.CP1(), scanner.CP2()
|
||||
segment.Verb = GeometryCubic
|
||||
segment.Control1, segment.Control2 = Point{X: c1.X, Y: -c1.Y}, Point{X: c2.X, Y: -c2.Y}
|
||||
case canvas.ArcToCmd:
|
||||
segment.Verb = GeometryArc
|
||||
segment.RadiusX, segment.RadiusY, segment.Rotation, segment.Large, segment.Sweep = scanner.Arc()
|
||||
segment.Rotation, segment.Sweep = -segment.Rotation, !segment.Sweep
|
||||
case canvas.CloseCmd:
|
||||
segment.Verb = GeometryClose
|
||||
}
|
||||
result = append(result, segment)
|
||||
}
|
||||
return &result
|
||||
}
|
||||
|
||||
// MeasureObject 使用默认解释器度量对象,不分配页面像素
|
||||
// 入参: r 渲染器, object 图形对象, options 度量上下文
|
||||
// 返回: ObjectMeasurement 度量结果, error 错误信息
|
||||
@@ -102,12 +37,13 @@ func (CanvasBackend) MeasureObject(r *Renderer, object GraphicObject, options Me
|
||||
return ObjectMeasurement{}, err
|
||||
}
|
||||
}
|
||||
clip, err := canvasGeometryPath(options.Clip)
|
||||
clip, err := geometryToCanvasPath(options.Clip)
|
||||
if err != nil {
|
||||
return ObjectMeasurement{}, err
|
||||
}
|
||||
bounds := &boundsRenderer{collect: options.Contours}
|
||||
renderer := *r
|
||||
renderer.renderError = nil
|
||||
renderer.pageText = nil
|
||||
renderer.textOnly = false
|
||||
defaults := options.Defaults
|
||||
@@ -127,7 +63,7 @@ func (CanvasBackend) MeasureObject(r *Renderer, object GraphicObject, options Me
|
||||
box = unionTextBox(box, glyph)
|
||||
}
|
||||
}
|
||||
return ObjectMeasurement{Bounds: box}, nil
|
||||
return ObjectMeasurement{Bounds: box}, renderer.renderError
|
||||
case "PathObject":
|
||||
obj := object.PathObject
|
||||
defaults = renderer.drawParamDefaults(obj.DrawParam, defaults)
|
||||
@@ -146,7 +82,7 @@ func (CanvasBackend) MeasureObject(r *Renderer, object GraphicObject, options Me
|
||||
case "CompositeObject", "CompositeGraphicUnit":
|
||||
renderer.renderCompositeGraphicUnit(ctx, object.CompositeGraphicUnit, 0, defaults, options.Parent, options.BoundaryInCTM, clip)
|
||||
}
|
||||
return ObjectMeasurement{Bounds: bounds.box, Contours: bounds.contours}, nil
|
||||
return ObjectMeasurement{Bounds: bounds.box, Contours: bounds.contours}, renderer.renderError
|
||||
}
|
||||
|
||||
// measureImage 复用图片变换与裁剪语义度量范围,不读取像素数据
|
||||
@@ -261,7 +197,7 @@ func (CanvasBackend) PageText(r *Renderer, page *PageContent) (*PageText, error)
|
||||
renderer.OnPageText = nil
|
||||
renderer.pageText = &PageText{}
|
||||
renderer.textOnly = true
|
||||
if err := renderer.renderPageToContext(canvas.NewContext(canvas.New(box.W, box.H)), page, false); err != nil {
|
||||
if err := renderer.renderCanvasPageToContext(canvas.NewContext(canvas.New(box.W, box.H)), page, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return renderer.pageText, nil
|
||||
@@ -38,7 +38,7 @@ func (CanvasBackend) RenderSVG(r *Renderer, page *PageContent, writer io.Writer,
|
||||
case SVGObjects:
|
||||
return r.renderSVGResources(page, writer, true, true)
|
||||
case SVGEmbedded:
|
||||
c, err := r.renderPage(page)
|
||||
c, err := r.renderCanvasPage(page)
|
||||
if err != nil {
|
||||
return SVGResources{}, err
|
||||
}
|
||||
@@ -58,7 +58,7 @@ func (CanvasBackend) RenderSVG(r *Renderer, page *PageContent, writer io.Writer,
|
||||
// 入参: r 渲染器, page 页面内容, writer 输出流
|
||||
// 返回: error 错误信息
|
||||
func (CanvasBackend) RenderEPS(r *Renderer, page *PageContent, writer io.Writer) error {
|
||||
c, err := r.renderPage(page)
|
||||
c, err := r.renderCanvasPage(page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -80,7 +80,10 @@ func (CanvasBackend) RenderPDF(r *Renderer, pages []RenderDocumentPage, writer i
|
||||
if len(pages) == 0 {
|
||||
return fmt.Errorf("no pages found")
|
||||
}
|
||||
navigation := newPDFNavigation(r, r.Reader.doc, pages)
|
||||
navigation, err := newPDFNavigation(r, r.Reader.doc, pages)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buf, direct := writer.(*bytes.Buffer)
|
||||
if !direct {
|
||||
buf = &bytes.Buffer{}
|
||||
@@ -98,7 +101,7 @@ func (CanvasBackend) RenderPDF(r *Renderer, pages []RenderDocumentPage, writer i
|
||||
p.NewPage(page.Box.W, page.Box.H)
|
||||
}
|
||||
navigation.apply(p, i)
|
||||
if err := r.renderPageToContext(canvas.NewContext(renderer), page.Content, true); err != nil {
|
||||
if err := r.renderCanvasPageToContext(canvas.NewContext(renderer), page.Content, !r.TransparentBackground); err != nil {
|
||||
buf.Truncate(start)
|
||||
return fmt.Errorf("failed to render page %d: %w", i+1, err)
|
||||
}
|
||||
@@ -118,7 +121,7 @@ func (CanvasBackend) RenderPDF(r *Renderer, pages []RenderDocumentPage, writer i
|
||||
if direct {
|
||||
return nil
|
||||
}
|
||||
_, err := writer.Write(data)
|
||||
_, err = writer.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// 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 (
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
// renderCanvasPage 渲染特定页面内容
|
||||
// 入参: page 页面内容
|
||||
// 返回: *canvas.Canvas 画布实例, error 错误信息
|
||||
func (r *Renderer) renderCanvasPage(page *PageContent) (*canvas.Canvas, error) {
|
||||
box, err := r.GetPageBox(page)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
width, height := box.W, box.H
|
||||
c := canvas.New(width, height)
|
||||
ctx := canvas.NewContext(c)
|
||||
if err := r.renderCanvasPageToContext(ctx, page, !r.TransparentBackground); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// renderCanvasContext 渲染页面到默认解释器上下文
|
||||
// 入参: ctx 画布上下文, page 页面内容
|
||||
// 返回: error 错误信息
|
||||
func (r *Renderer) renderCanvasContext(ctx *canvas.Context, page *PageContent) error {
|
||||
renderer := *r
|
||||
renderer.decodeImages = true
|
||||
return renderer.renderCanvasPageToContext(ctx, page, !r.TransparentBackground)
|
||||
}
|
||||
|
||||
// renderCanvasPageToContext 渲染页面到指定上下文
|
||||
// 入参: ctx 画布上下文, page 页面内容, drawBackground 是否绘制页面背景
|
||||
// 返回: error 错误信息
|
||||
func (r *Renderer) renderCanvasPageToContext(ctx *canvas.Context, page *PageContent, drawBackground bool) error {
|
||||
r.renderError = nil
|
||||
box, err := r.GetPageBox(page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.OnPageText != nil {
|
||||
renderer := *r
|
||||
renderer.pageText = &PageText{}
|
||||
r = &renderer
|
||||
}
|
||||
pageH := box.H
|
||||
if drawBackground {
|
||||
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 r.renderError != nil {
|
||||
return r.renderError
|
||||
}
|
||||
if r.OnPageText != nil {
|
||||
r.OnPageText(page, r.pageText)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// 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 (
|
||||
"github.com/tdewolff/canvas"
|
||||
"math"
|
||||
)
|
||||
|
||||
// parseFillPaint 解析填充画刷
|
||||
// 入参: fillColor 填充颜色节点, x X坐标, y Y坐标, pageH 页面高度
|
||||
// 返回: any 填充画刷
|
||||
func (r *Renderer) parseFillPaint(fillColor *FillColor, x, y, pageH float64) any {
|
||||
if fillColor == nil {
|
||||
return nil
|
||||
}
|
||||
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 解析勾边画刷
|
||||
// 入参: strokeColor 勾边颜色节点, x X坐标, y Y坐标, pageH 页面高度
|
||||
// 返回: any 勾边画刷
|
||||
func (r *Renderer) parseStrokePaint(strokeColor *StrokeColor, x, y, pageH float64) any {
|
||||
return r.parseFillPaint((*FillColor)(strokeColor), x, y, pageH)
|
||||
}
|
||||
|
||||
// parseShdSegments 将通用渐变分段转换为Canvas画刷
|
||||
// 入参: segments 渐变分段, alpha 透明度
|
||||
// 返回: canvas.Grad 渐变分段
|
||||
func (r *Renderer) parseShdSegments(segments []ShdSegment, alpha *int) canvas.Grad {
|
||||
var gradient canvas.Grad
|
||||
for _, stop := range r.GradientStops(segments, alpha) {
|
||||
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
|
||||
}
|
||||
start := parseFloats(axialShd.StartPoint)
|
||||
end := parseFloats(axialShd.EndPoint)
|
||||
if len(start) < 2 || len(end) < 2 {
|
||||
return nil
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// axialShdClip 获取轴向渐变的延伸裁剪区域
|
||||
// 入参: gradient 轴向渐变, extend 延伸方向, bounds 可见区域
|
||||
// 返回: *canvas.Path 裁剪区域
|
||||
func axialShdClip(gradient *canvas.LinearGradient, extend int, bounds canvas.Rect) *canvas.Path {
|
||||
if extend == 3 {
|
||||
return nil
|
||||
}
|
||||
d := gradient.End.Sub(gradient.Start)
|
||||
axis := canvas.Matrix{{d.X, -d.Y, gradient.Start.X}, {d.Y, d.X, gradient.Start.Y}}
|
||||
area := bounds.Transform(axis.Inv())
|
||||
if extend&1 == 0 {
|
||||
area.X0 = math.Max(area.X0, 0)
|
||||
}
|
||||
if extend&2 == 0 {
|
||||
area.X1 = math.Min(area.X1, 1)
|
||||
}
|
||||
if area.X1 <= area.X0 {
|
||||
return &canvas.Path{}
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -23,9 +23,6 @@ import (
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
const defaultPathLineWidth = 0.353
|
||||
const defaultMiterLimit = 3.528
|
||||
|
||||
// pathStyle 路径绘制样式
|
||||
type pathStyle struct {
|
||||
fillColor color.Color
|
||||
@@ -311,9 +308,9 @@ func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *patternPaint, pag
|
||||
return
|
||||
}
|
||||
if r.pageText != nil {
|
||||
renderer := *r
|
||||
renderer.pageText = nil
|
||||
r = &renderer
|
||||
text := r.pageText
|
||||
r.pageText = nil
|
||||
defer func() { r.pageText = text }()
|
||||
}
|
||||
if clip == nil || clip.Empty() || len(pattern.CellContent.Objects) == 0 {
|
||||
return
|
||||
@@ -456,186 +453,6 @@ func (r *Renderer) buildPath(obj PathObject, pageH float64, ctm Matrix, boundary
|
||||
return p.Transform(canvas.Matrix{{ctm.a, ctm.c, ctm.e}, {-ctm.b, -ctm.d, pageH - ctm.f}})
|
||||
}
|
||||
|
||||
// buildObjectClipPath 构建对象裁剪路径并应用父级变换
|
||||
// 入参: 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 !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},
|
||||
})
|
||||
}
|
||||
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 {
|
||||
renderer := &clipRenderer{}
|
||||
for _, area := range clip.Area {
|
||||
areaCTM := NewMatrix(area.CTM)
|
||||
if clips.TransFlag == nil || *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 pathObj.Rule == "Even-Odd" {
|
||||
cp = cp.Settle(canvas.EvenOdd)
|
||||
}
|
||||
renderer.add(cp)
|
||||
}
|
||||
for _, textObj := range area.Text {
|
||||
box, _ := ParseBox(textObj.Boundary)
|
||||
ctm := NewMatrix(textObj.CTM)
|
||||
m := areaCTM.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(ctm)
|
||||
ctx := canvas.NewContext(renderer)
|
||||
ctx.SetView(canvas.Matrix{{m.a, -m.c, bx + m.e}, {-m.b, m.d, pageH - by - m.f}})
|
||||
renderer.clip = r.buildObjectClipPath(textObj.Clips, pageH, box.X, box.Y, ctm, &areaCTM, true)
|
||||
if renderer.clip != nil {
|
||||
renderer.clip = renderer.clip.Translate(bx, -by)
|
||||
}
|
||||
textObj.Boundary, textObj.CTM = "", ""
|
||||
textObj.Clips = nil
|
||||
textObj.Alpha = nil
|
||||
textObj.FillColor = &FillColor{Value: "0 0 0"}
|
||||
textObj.StrokeColor = &StrokeColor{Value: "0 0 0"}
|
||||
textRenderer := *r
|
||||
textRenderer.pageText = nil
|
||||
textRenderer.textOnly = false
|
||||
textRenderer.renderText(ctx, textObj, 0, r.drawParamDefaults(area.DrawParam, nil), nil, false, nil)
|
||||
}
|
||||
}
|
||||
if renderer.path == nil {
|
||||
renderer.path = &canvas.Path{}
|
||||
}
|
||||
p = intersectClipPath(p, renderer.path)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// intersectClipPath 求裁剪路径交集
|
||||
// 入参: parent 父级裁剪路径, current 当前裁剪路径
|
||||
// 返回: *canvas.Path 相交后的裁剪路径
|
||||
func intersectClipPath(parent, current *canvas.Path) *canvas.Path {
|
||||
if parent == nil {
|
||||
return current
|
||||
}
|
||||
if current == nil {
|
||||
return parent
|
||||
}
|
||||
parentRect, parentOK := rectangularPath(parent)
|
||||
currentRect, currentOK := rectangularPath(current)
|
||||
if parentOK && currentOK {
|
||||
return parentRect.And(currentRect).ToPath()
|
||||
}
|
||||
if parent.Empty() || current.Empty() {
|
||||
return &canvas.Path{}
|
||||
}
|
||||
if parentOK && parentRect.Contains(current.FastBounds()) {
|
||||
return current
|
||||
}
|
||||
if currentOK && currentRect.Contains(parent.FastBounds()) {
|
||||
return parent
|
||||
}
|
||||
return parent.And(current)
|
||||
}
|
||||
|
||||
// unionClipPath 合并裁剪区域
|
||||
// 入参: left 左侧裁剪路径, right 右侧裁剪路径
|
||||
// 返回: *canvas.Path 合并后的裁剪路径
|
||||
func unionClipPath(left, right *canvas.Path) *canvas.Path {
|
||||
leftRect, leftOK := rectangularPath(left)
|
||||
rightRect, rightOK := rectangularPath(right)
|
||||
if leftOK && rightOK {
|
||||
bounds := leftRect.Add(rightRect)
|
||||
area := leftRect.Area() + rightRect.Area() - leftRect.And(rightRect).Area()
|
||||
if canvas.Equal(area, bounds.Area()) {
|
||||
return bounds.ToPath()
|
||||
}
|
||||
}
|
||||
return left.Or(right)
|
||||
}
|
||||
|
||||
// applyClipPath 应用裁剪路径
|
||||
// 入参: path 绘制路径, clip 裁剪路径
|
||||
// 返回: *canvas.Path 裁剪后的绘制路径
|
||||
func applyClipPath(path, clip *canvas.Path) *canvas.Path {
|
||||
if path == nil || clip == nil {
|
||||
return path
|
||||
}
|
||||
if path.Empty() || clip.Empty() {
|
||||
return &canvas.Path{}
|
||||
}
|
||||
if rect, ok := rectangularPath(clip); ok {
|
||||
bounds := path.FastBounds()
|
||||
if rect.Contains(bounds) {
|
||||
return path
|
||||
}
|
||||
if !rect.Overlaps(bounds) {
|
||||
return &canvas.Path{}
|
||||
}
|
||||
if pathRect, ok := rectangularPath(path); ok {
|
||||
return pathRect.And(rect).ToPath()
|
||||
}
|
||||
}
|
||||
return path.And(clip)
|
||||
}
|
||||
|
||||
// rectangularPath 获取矩形路径区域
|
||||
// 入参: path 路径对象
|
||||
// 返回: canvas.Rect 矩形区域, bool 是否为矩形
|
||||
func rectangularPath(path *canvas.Path) (canvas.Rect, bool) {
|
||||
if path == nil || path.HasSubpaths() || !path.Closed() {
|
||||
return canvas.Rect{}, false
|
||||
}
|
||||
data := path.Data()
|
||||
for i := 0; i < len(data); i += 4 {
|
||||
if i+4 > len(data) || (data[i] != canvas.MoveToCmd && data[i] != canvas.LineToCmd && data[i] != canvas.CloseCmd) {
|
||||
return canvas.Rect{}, false
|
||||
}
|
||||
}
|
||||
points := path.Coords()
|
||||
if len(points) != 5 || !points[0].Equals(points[4]) {
|
||||
return canvas.Rect{}, false
|
||||
}
|
||||
rect := path.FastBounds()
|
||||
corners := 0
|
||||
for _, point := range points[:4] {
|
||||
corner := 0
|
||||
switch {
|
||||
case canvas.Equal(point.X, rect.X0) && canvas.Equal(point.Y, rect.Y0):
|
||||
corner = 1
|
||||
case canvas.Equal(point.X, rect.X1) && canvas.Equal(point.Y, rect.Y0):
|
||||
corner = 2
|
||||
case canvas.Equal(point.X, rect.X1) && canvas.Equal(point.Y, rect.Y1):
|
||||
corner = 4
|
||||
case canvas.Equal(point.X, rect.X0) && canvas.Equal(point.Y, rect.Y1):
|
||||
corner = 8
|
||||
default:
|
||||
return canvas.Rect{}, false
|
||||
}
|
||||
if corners&corner != 0 {
|
||||
return canvas.Rect{}, false
|
||||
}
|
||||
corners |= corner
|
||||
}
|
||||
return rect, corners == 15
|
||||
}
|
||||
|
||||
// buildTinyFillRectPath 构建微小填充矩形路径
|
||||
// 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵, bx 边界X坐标, by 边界Y坐标
|
||||
// 返回: *canvas.Path 路径对象
|
||||
@@ -71,8 +71,8 @@ type pdfOutline struct {
|
||||
|
||||
// newPDFNavigation 创建PDF导航信息
|
||||
// 入参: renderer 渲染器, doc 文档结构, pages 页面数据
|
||||
// 返回: *pdfNavigation PDF导航信息
|
||||
func newPDFNavigation(renderer *Renderer, doc *Document, pages []RenderDocumentPage) *pdfNavigation {
|
||||
// 返回: *pdfNavigation PDF导航信息, error 几何错误
|
||||
func newPDFNavigation(renderer *Renderer, doc *Document, pages []RenderDocumentPage) (*pdfNavigation, error) {
|
||||
navigation := &pdfNavigation{
|
||||
Anchor: make(map[int][]pdfAnchor),
|
||||
Link: make(map[int][]pdfLink),
|
||||
@@ -98,7 +98,10 @@ func newPDFNavigation(renderer *Renderer, doc *Document, pages []RenderDocumentP
|
||||
if action.Event != "CLICK" {
|
||||
continue
|
||||
}
|
||||
box, _ := actionLinkRegion(source, action)
|
||||
box, _, err := renderer.actionLinkRegion(source, action)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if box.W <= 0 || box.H <= 0 {
|
||||
continue
|
||||
}
|
||||
@@ -110,7 +113,7 @@ func newPDFNavigation(renderer *Renderer, doc *Document, pages []RenderDocumentP
|
||||
if doc != nil {
|
||||
navigation.addOutlines(doc.Outlines.OutlineElem, 0, bookmarks, pageIndex, pages)
|
||||
}
|
||||
return navigation
|
||||
return navigation, nil
|
||||
}
|
||||
|
||||
// addAction 添加PDF动作
|
||||
@@ -173,25 +176,6 @@ func (n *pdfNavigation) apply(renderer *pdf.PDF, page int) {
|
||||
}
|
||||
}
|
||||
|
||||
// outlineDest 获取大纲跳转目标
|
||||
// 入参: outline 大纲节点, bookmarks 书签
|
||||
// 返回: *Dest 跳转目标
|
||||
func outlineDest(outline OutlineElem, bookmarks map[string]Dest) *Dest {
|
||||
for _, action := range outline.Actions {
|
||||
if action.Goto != nil {
|
||||
if dest := gotoDest(action.Goto, bookmarks); dest != nil {
|
||||
return dest
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, child := range outline.OutlineElem {
|
||||
if dest := outlineDest(child, bookmarks); dest != nil {
|
||||
return dest
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pdfSourceRect 转换PDF动作区域
|
||||
// 入参: box OFD区域, pageH 页面高度
|
||||
// 返回: canvas.Rect PDF区域
|
||||
File renamed without changes.
@@ -17,12 +17,8 @@ package ofdgo
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"math"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
"github.com/tdewolff/canvas/renderers/rasterizer"
|
||||
)
|
||||
|
||||
// renderStamp 渲染印章
|
||||
@@ -56,8 +52,12 @@ func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) {
|
||||
ctx.Push()
|
||||
ctx.Translate(s.Box.X, pageH-(s.Box.Y+s.Box.H))
|
||||
ctx.Scale(s.Box.W/sealBox.W, s.Box.H/sealBox.H)
|
||||
renderer.renderPageToContext(ctx, content, false)
|
||||
err = renderer.renderCanvasPageToContext(ctx, content, false)
|
||||
ctx.Pop()
|
||||
if err != nil {
|
||||
r.renderError = err
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -90,20 +90,18 @@ func (r *Renderer) renderOFDStampImage(data []byte) image.Image {
|
||||
}
|
||||
renderer := r.childRenderer(reader)
|
||||
renderer.decodeImages = true
|
||||
renderer.TransparentBackground = true
|
||||
for _, pageRef := range doc.Pages.Page {
|
||||
content, err := reader.PageContent(pageRef)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
sealBox, err := renderer.GetPageBox(content)
|
||||
img, err := renderer.RenderToImage(content)
|
||||
if err != nil {
|
||||
continue
|
||||
r.renderError = err
|
||||
return nil
|
||||
}
|
||||
c := canvas.New(sealBox.W, sealBox.H)
|
||||
if err := renderer.renderPageToContext(canvas.NewContext(c), content, false); err != nil {
|
||||
continue
|
||||
}
|
||||
return rasterizer.Draw(c, canvas.DPMM(r.DPI/25.4), canvas.DefaultColorSpace)
|
||||
return img
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -125,51 +123,3 @@ func (r *Renderer) renderStampImage(ctx *canvas.Context, img image.Image, s Stam
|
||||
ctx.DrawImage(0, 0, img, canvas.DPMM(1.0))
|
||||
ctx.Pop()
|
||||
}
|
||||
|
||||
// clipStampImage 裁剪印章图像
|
||||
// 入参: img 印章图像, box 印章区域, clip 裁剪区域
|
||||
// 返回: image.Image 裁剪后的印章图像
|
||||
func clipStampImage(img image.Image, box, clip Box) image.Image {
|
||||
bounds := img.Bounds()
|
||||
x0 := int(math.Floor(clip.X / box.W * float64(bounds.Dx())))
|
||||
y0 := int(math.Floor(clip.Y / box.H * float64(bounds.Dy())))
|
||||
x1 := int(math.Ceil((clip.X + clip.W) / box.W * float64(bounds.Dx())))
|
||||
y1 := int(math.Ceil((clip.Y + clip.H) / box.H * float64(bounds.Dy())))
|
||||
out := image.NewNRGBA(image.Rect(0, 0, x1-x0, y1-y0))
|
||||
draw.Draw(out, out.Bounds(), img, image.Pt(bounds.Min.X+x0, bounds.Min.Y+y0), draw.Src)
|
||||
return out
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -80,11 +80,11 @@ func (r *Renderer) renderSVGResources(page *PageContent, writer io.Writer, image
|
||||
s.objects[object] = editorObjectID(*object)
|
||||
}
|
||||
}
|
||||
if err := r.renderPageToContext(canvas.NewContext(s), page, true); err != nil {
|
||||
if err := r.renderCanvasPageToContext(canvas.NewContext(s), page, !r.TransparentBackground); err != nil {
|
||||
return SVGResources{}, err
|
||||
}
|
||||
} else {
|
||||
c, err := r.renderPage(page)
|
||||
c, err := r.renderCanvasPage(page)
|
||||
if err != nil {
|
||||
return SVGResources{}, err
|
||||
}
|
||||
@@ -188,7 +188,7 @@ func (s *svgResourceRenderer) RenderText(text *canvas.Text, m canvas.Matrix) {
|
||||
font := text.MostCommonFontFace().Font
|
||||
resource, ok := s.renderer.svgFontCache[font]
|
||||
if !ok {
|
||||
data := font.SFNT.Write()
|
||||
data := fontSFNTData(font.SFNT)
|
||||
resource = SVGFont{
|
||||
Name: fmt.Sprintf("ofdgo-%x-%d", sha256.Sum256(data), font.Style()),
|
||||
Weight: font.Style().CSS(),
|
||||
File renamed without changes.
+3
-17
@@ -19,28 +19,14 @@ import (
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
// Outline 获取页面坐标系中的SVG路径轮廓,不包含描边宽度和填充
|
||||
// Outline 使用默认几何后端获取SVG轮廓,自定义后端使用Renderer.PathOutline
|
||||
// 入参: p 路径对象
|
||||
// 返回: string SVG路径数据, error 错误信息
|
||||
func (p PathObject) Outline() (string, error) {
|
||||
if err := creationPath(p.AbbreviatedData); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := creationBox(p.Boundary); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if p.CTM != "" {
|
||||
if _, err := creationNumbers(p.CTM, 6); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
r := &Renderer{}
|
||||
path := r.buildPath(p, 0, NewMatrix(p.CTM), false)
|
||||
return path.Transform(canvas.Matrix{{1, 0, 0}, {0, -1, 0}}).ToSVG(), nil
|
||||
r := &Renderer{backends: defaultRenderBackends()}
|
||||
return r.PathOutline(p)
|
||||
}
|
||||
|
||||
// ShapeKind 基本图形类型
|
||||
|
||||
@@ -19,10 +19,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
canvastext "github.com/tdewolff/canvas/text"
|
||||
"github.com/tdewolff/font"
|
||||
)
|
||||
|
||||
// Text 获取定位后的文字,横向换行以换行符分隔,字形索引以占位字符表示
|
||||
@@ -77,26 +73,6 @@ type textGlyph struct {
|
||||
GlyphID int
|
||||
}
|
||||
|
||||
// textGlyphPathCacheKey 字形路径缓存键
|
||||
type textGlyphPathCacheKey struct {
|
||||
font *canvas.Font
|
||||
size float64
|
||||
fauxBold float64
|
||||
fauxItalic float64
|
||||
xOffset int32
|
||||
yOffset int32
|
||||
language string
|
||||
script canvastext.Script
|
||||
direction canvastext.Direction
|
||||
glyph textGlyph
|
||||
}
|
||||
|
||||
// textGlyphPathCacheValue 字形路径缓存值
|
||||
type textGlyphPathCacheValue struct {
|
||||
path *canvas.Path
|
||||
width float64
|
||||
}
|
||||
|
||||
// textGlyphTransform 字符到字形变换
|
||||
type textGlyphTransform struct {
|
||||
CodeCount int
|
||||
@@ -193,66 +169,6 @@ func (r *Renderer) textGlyphFromID(fontID string, glyphID int) textGlyph {
|
||||
return textGlyph{GlyphID: glyphID}
|
||||
}
|
||||
|
||||
// textGlyphWidth 获取绘制字形宽度
|
||||
// 入参: face 字体, glyph 绘制字形
|
||||
// 返回: float64 字形宽度
|
||||
func textGlyphWidth(face *canvas.FontFace, glyph textGlyph) float64 {
|
||||
if glyph.GlyphID >= 0 && glyph.GlyphID <= 0xFFFF {
|
||||
return face.MmPerEm * float64(face.Font.GlyphAdvance(uint16(glyph.GlyphID)))
|
||||
}
|
||||
return face.TextWidth(glyph.Text)
|
||||
}
|
||||
|
||||
// textGlyphPath 获取绘制字形路径
|
||||
// 入参: face 字体, glyph 绘制字形
|
||||
// 返回: *canvas.Path 字形路径, float64 字形宽度
|
||||
func textGlyphPath(face *canvas.FontFace, glyph textGlyph) (*canvas.Path, float64) {
|
||||
if glyph.GlyphID < 0 || glyph.GlyphID > 0xFFFF {
|
||||
return face.ToPath(glyph.Text)
|
||||
}
|
||||
p := &canvas.Path{}
|
||||
glyphID := uint16(glyph.GlyphID)
|
||||
_ = face.Font.GlyphPath(p, glyphID, face.PPEM(canvas.DefaultResolution), 0, 0, face.MmPerEm, font.NoHinting)
|
||||
if face.FauxBold != 0 {
|
||||
d := face.FauxBold * face.Size
|
||||
if face.Font.IsTrueType {
|
||||
d = -d
|
||||
}
|
||||
origFastStroke := canvas.FastStroke
|
||||
canvas.FastStroke = true
|
||||
p = p.Offset(d, canvas.Tolerance)
|
||||
canvas.FastStroke = origFastStroke
|
||||
}
|
||||
if face.FauxItalic != 0 {
|
||||
p = p.Transform(canvas.Identity.Shear(face.FauxItalic, 0))
|
||||
}
|
||||
return p, face.MmPerEm * float64(face.Font.GlyphAdvance(glyphID))
|
||||
}
|
||||
|
||||
// cachedTextGlyphPath 获取缓存的字形路径
|
||||
// 入参: face 字体, glyph 绘制字形
|
||||
// 返回: *canvas.Path 字形路径, float64 字形宽度
|
||||
func (r *Renderer) cachedTextGlyphPath(face *canvas.FontFace, glyph textGlyph) (*canvas.Path, float64) {
|
||||
key := textGlyphPathCacheKey{
|
||||
font: face.Font,
|
||||
size: face.Size,
|
||||
fauxBold: face.FauxBold,
|
||||
fauxItalic: face.FauxItalic,
|
||||
xOffset: face.XOffset,
|
||||
yOffset: face.YOffset,
|
||||
language: face.Language,
|
||||
script: face.Script,
|
||||
direction: face.Direction,
|
||||
glyph: glyph,
|
||||
}
|
||||
if cached, ok := r.textGlyphPathCache[key]; ok {
|
||||
return cached.path, cached.width
|
||||
}
|
||||
path, width := textGlyphPath(face, glyph)
|
||||
r.textGlyphPathCache[key] = textGlyphPathCacheValue{path: path, width: width}
|
||||
return path, width
|
||||
}
|
||||
|
||||
// hasTextMatrix 判断文本是否需要应用字形变换
|
||||
// 入参: ctm 变换矩阵
|
||||
// 返回: bool 是否需要变换
|
||||
@@ -261,16 +177,6 @@ func hasTextMatrix(ctm Matrix) bool {
|
||||
return math.Abs(ctm.a-1) > eps || math.Abs(ctm.b) > eps || math.Abs(ctm.c) > eps || math.Abs(ctm.d-1) > eps
|
||||
}
|
||||
|
||||
// textMatrix 获取文本字形变换矩阵
|
||||
// 入参: ctm OFD变换矩阵
|
||||
// 返回: canvas.Matrix 画布变换矩阵
|
||||
func textMatrix(ctm Matrix) canvas.Matrix {
|
||||
return canvas.Matrix{
|
||||
{ctm.a, -ctm.c, 0},
|
||||
{-ctm.b, ctm.d, 0},
|
||||
}
|
||||
}
|
||||
|
||||
// fontGlyphRune 获取字形ID对应的包装字体字符
|
||||
// 入参: fontID 字体ID, glyphID 字形ID或CID
|
||||
// 返回: rune 包装字体字符, bool 是否存在
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// LayoutText 按自有字体度量重排横向文字,保留绘制属性,不进行复杂文字塑形
|
||||
// 不修改文档,通过AddObject或UpdateObject提交;选项仅供当前编辑过程使用,保存为标准文字定位
|
||||
// 入参: obj 文字对象, value 原文, options 段落排版选项, metrics 字体度量
|
||||
// 返回: error 错误信息
|
||||
func LayoutText(obj *TextObject, value string, options TextLayout, metrics FontMetrics) error {
|
||||
if metrics.UnitsPerEm() == 0 {
|
||||
return fmt.Errorf("font units per em must be positive")
|
||||
}
|
||||
if obj.ReadDirection != 0 || obj.CharDirection != 0 {
|
||||
return fmt.Errorf("automatic text layout requires horizontal text")
|
||||
}
|
||||
if !finite(obj.Size) || obj.Size <= 0 || !finite(obj.HScale) || obj.HScale < 0 {
|
||||
return fmt.Errorf("invalid text dimensions")
|
||||
}
|
||||
value = strings.ReplaceAll(strings.ReplaceAll(value, "\r\n", "\n"), "\r", "\n")
|
||||
if !utf8.ValidString(value) || strings.Contains(value, "\t") || strings.Trim(value, "\n") == "" {
|
||||
return fmt.Errorf("text must contain UTF-8 characters without tabs")
|
||||
}
|
||||
lineHeight := options.LineHeight
|
||||
if !finite(lineHeight) || lineHeight < 0 {
|
||||
return fmt.Errorf("line height must be finite and nonnegative")
|
||||
}
|
||||
if !finite(options.LetterSpacing) {
|
||||
return fmt.Errorf("letter spacing must be finite")
|
||||
}
|
||||
if !finite(options.LeftIndent) || !finite(options.RightIndent) || !finite(options.FirstLineIndent) {
|
||||
return fmt.Errorf("paragraph indents must be finite")
|
||||
}
|
||||
if !slices.Contains([]string{"", "left", "center", "right", "justify"}, options.Align) {
|
||||
return fmt.Errorf("invalid text alignment %q", options.Align)
|
||||
}
|
||||
var width float64
|
||||
if options.Wrap || options.Align != "" && options.Align != "left" || options.LeftIndent != 0 || options.RightIndent != 0 || options.FirstLineIndent != 0 {
|
||||
box, err := obj.TextFrame()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
width = box.W - options.LeftIndent - options.RightIndent
|
||||
if !finite(width) || width <= 0 || !finite(width-options.FirstLineIndent) || width-options.FirstLineIndent <= 0 {
|
||||
return fmt.Errorf("paragraph indents leave no text width")
|
||||
}
|
||||
}
|
||||
hScale := obj.HScale
|
||||
if hScale == 0 {
|
||||
hScale = 1
|
||||
}
|
||||
ascender, descender, gap := metrics.VerticalMetrics()
|
||||
unit := obj.Size / float64(metrics.UnitsPerEm())
|
||||
if lineHeight == 0 {
|
||||
lineHeight = math.Max(obj.Size, float64(int(ascender)+int(descender)+int(gap))*unit)
|
||||
}
|
||||
var codes []TextCode
|
||||
var missing []rune
|
||||
for _, paragraph := range strings.Split(value, "\n") {
|
||||
runes := []rune(paragraph)
|
||||
advances := make([]float64, len(runes))
|
||||
for i, char := range runes {
|
||||
glyph := metrics.GlyphIndex(char)
|
||||
if glyph == 0 {
|
||||
missing = append(missing, char)
|
||||
continue
|
||||
}
|
||||
advances[i] = float64(metrics.GlyphAdvance(glyph)) * unit * hScale
|
||||
}
|
||||
if len(missing) != 0 {
|
||||
continue
|
||||
}
|
||||
spaceTextAdvances(runes, advances, options.LetterSpacing)
|
||||
for _, advance := range advances {
|
||||
if !finite(advance) {
|
||||
return fmt.Errorf("text advance exceeds finite range")
|
||||
}
|
||||
}
|
||||
lines := breakTextLines(runes, advances, width, options.Wrap, options.LetterSpacing, options.FirstLineIndent)
|
||||
for i, line := range lines {
|
||||
indent := 0.0
|
||||
if i == 0 {
|
||||
indent = options.FirstLineIndent
|
||||
}
|
||||
x, deltas := alignTextLine(runes[line[0]:line[1]], advances[line[0]:line[1]], width-indent, options.Align, i+1 < len(lines), options.LetterSpacing)
|
||||
x += options.LeftIndent + indent
|
||||
y := float64(ascender)*unit + float64(len(codes))*lineHeight
|
||||
if !finite(x) || !finite(y) {
|
||||
return fmt.Errorf("text position exceeds finite range")
|
||||
}
|
||||
codes = append(codes, TextCode{X: ofdNumber(x), Y: ofdNumber(y), DeltaX: deltas, Value: escapeOFDText(string(runes[line[0]:line[1]]))})
|
||||
}
|
||||
}
|
||||
if err := missingGlyphError(obj.Font, missing); err != nil {
|
||||
return err
|
||||
}
|
||||
obj.TextCode = codes
|
||||
obj.layout = nil
|
||||
if options != (TextLayout{}) {
|
||||
obj.layout = &textLayout{value: value, options: options}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user