mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
1 parent
e78ea6f919
commit
3b9f88cbba
16 files changed
+383
-43
No files matched your search
@@ -9,7 +9,7 @@ require (
|
|||||||
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
||||||
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
|
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
|
||||||
github.com/xiaoqidun/jbig2 v1.1.1
|
github.com/xiaoqidun/jbig2 v1.1.1
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260927050705-f09ba3f6aa16
|
github.com/xiaoqidun/pdfgo v0.0.0-20260927070002-9eedb7bf8e13
|
||||||
golang.org/x/crypto v0.57.0
|
golang.org/x/crypto v0.57.0
|
||||||
golang.org/x/image v0.46.0
|
golang.org/x/image v0.46.0
|
||||||
golang.org/x/text v0.42.0
|
golang.org/x/text v0.42.0
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
|
|||||||
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
|
github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
|
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260927050705-f09ba3f6aa16 h1:yzUX5/pLl38Nczw+yQXck/QZ7sPblNvKu5+55cH6ONM=
|
github.com/xiaoqidun/pdfgo v0.0.0-20260927070002-9eedb7bf8e13 h1:P3UEKffubpK6Cqh1ulQysufcw3c+zhEsUv4z+EhqCDA=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20260927050705-f09ba3f6aa16/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
|
github.com/xiaoqidun/pdfgo v0.0.0-20260927070002-9eedb7bf8e13/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// normalizeCmap 转换旧式双字节子表,保留各编码记录及原字形编号
|
||||||
|
// 入参: data 字符映射表
|
||||||
|
// 返回: []byte 等价字符映射表, error 子表错误
|
||||||
|
func normalizeCmap(data []byte) ([]byte, error) {
|
||||||
|
if len(data) < 4 {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
count := int(binary.BigEndian.Uint16(data[2:]))
|
||||||
|
if count > (len(data)-4)/8 {
|
||||||
|
return nil, fmt.Errorf("invalid cmap encoding records")
|
||||||
|
}
|
||||||
|
var out []byte
|
||||||
|
converted := make(map[uint32]uint32)
|
||||||
|
for n := 0; n < count; n++ {
|
||||||
|
record := 8 + 8*n
|
||||||
|
offset := binary.BigEndian.Uint32(data[record:])
|
||||||
|
if uint64(offset)+2 > uint64(len(data)) {
|
||||||
|
return nil, fmt.Errorf("invalid cmap subtable offset")
|
||||||
|
}
|
||||||
|
if binary.BigEndian.Uint16(data[offset:]) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if out == nil {
|
||||||
|
out = bytes.Clone(data)
|
||||||
|
}
|
||||||
|
next, ok := converted[offset]
|
||||||
|
if !ok {
|
||||||
|
mapping, err := parseCmapFormat2(data[offset:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
table := buildCmapTable(0, mapping)
|
||||||
|
subtable := table[binary.BigEndian.Uint32(table[8:]):]
|
||||||
|
language := binary.BigEndian.Uint16(data[offset+4:])
|
||||||
|
if binary.BigEndian.Uint16(subtable) == 12 {
|
||||||
|
binary.BigEndian.PutUint32(subtable[8:], uint32(language))
|
||||||
|
} else {
|
||||||
|
binary.BigEndian.PutUint16(subtable[4:], language)
|
||||||
|
}
|
||||||
|
next = uint32(len(out))
|
||||||
|
converted[offset] = next
|
||||||
|
out = append(out, subtable...)
|
||||||
|
}
|
||||||
|
binary.BigEndian.PutUint32(out[record:], next)
|
||||||
|
}
|
||||||
|
if out == nil {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseCmapFormat2 解析混合单字节与双字节字符的子头映射
|
||||||
|
// 入参: data format 2子表
|
||||||
|
// 返回: map[rune]uint16 编码值到字形编号, error 子头或字形数组越界
|
||||||
|
func parseCmapFormat2(data []byte) (map[rune]uint16, error) {
|
||||||
|
if len(data) < 526 {
|
||||||
|
return nil, fmt.Errorf("truncated cmap format 2")
|
||||||
|
}
|
||||||
|
length := int(binary.BigEndian.Uint16(data[2:]))
|
||||||
|
if length < 526 || length > len(data) {
|
||||||
|
return nil, fmt.Errorf("invalid cmap format 2 length")
|
||||||
|
}
|
||||||
|
data = data[:length]
|
||||||
|
maxKey := 0
|
||||||
|
for n := 0; n < 256; n++ {
|
||||||
|
key := int(binary.BigEndian.Uint16(data[6+2*n:]))
|
||||||
|
if key%8 != 0 || 518+key+8 > len(data) {
|
||||||
|
return nil, fmt.Errorf("invalid cmap format 2 subheader")
|
||||||
|
}
|
||||||
|
maxKey = max(maxKey, key)
|
||||||
|
}
|
||||||
|
mapping := make(map[rune]uint16)
|
||||||
|
for high := 0; high < 256; high++ {
|
||||||
|
key := 0
|
||||||
|
if high != 0 {
|
||||||
|
key = int(binary.BigEndian.Uint16(data[6+2*high:]))
|
||||||
|
if key == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pos := 518 + key
|
||||||
|
first, count := int(binary.BigEndian.Uint16(data[pos:])), int(binary.BigEndian.Uint16(data[pos+2:]))
|
||||||
|
delta := binary.BigEndian.Uint16(data[pos+4:])
|
||||||
|
start := pos + 6 + int(binary.BigEndian.Uint16(data[pos+6:]))
|
||||||
|
if first+count > 256 || count > 0 && (start < 526+maxKey || start+count*2 > len(data)) {
|
||||||
|
return nil, fmt.Errorf("invalid cmap format 2 glyph array")
|
||||||
|
}
|
||||||
|
for n := 0; n < count; n++ {
|
||||||
|
low := first + n
|
||||||
|
if high == 0 && binary.BigEndian.Uint16(data[6+2*low:]) != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
glyph := binary.BigEndian.Uint16(data[start+2*n:])
|
||||||
|
if glyph != 0 {
|
||||||
|
mapping[rune(high*256+low)] = glyph + delta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mapping, nil
|
||||||
|
}
|
||||||
+15
-2
@@ -89,11 +89,24 @@ func (f FontFile) Face(index int) ([]byte, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if bytes.HasPrefix(data, []byte("ttcf")) {
|
if bytes.HasPrefix(data, []byte("ttcf")) {
|
||||||
return extractCollectionFont(data, index)
|
data, err = extractCollectionFont(data, index)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
index = 0
|
||||||
}
|
}
|
||||||
if _, err := fontFileTables(data, index); err != nil {
|
tables, err := fontFileTables(data, index)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
cmap, err := normalizeCmap(tables["cmap"])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !bytes.Equal(cmap, tables["cmap"]) {
|
||||||
|
tables["cmap"] = cmap
|
||||||
|
return serializeOTF(tables)
|
||||||
|
}
|
||||||
return bytes.Clone(data), nil
|
return bytes.Clone(data), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -53,6 +54,14 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin
|
|||||||
if existingTables["CFF "] != nil {
|
if existingTables["CFF "] != nil {
|
||||||
isCFFSfnt = true
|
isCFFSfnt = true
|
||||||
}
|
}
|
||||||
|
cmap, err := normalizeCmap(existingTables["cmap"])
|
||||||
|
if err != nil {
|
||||||
|
return false, data, nil, false, err
|
||||||
|
}
|
||||||
|
if !bytes.Equal(cmap, existingTables["cmap"]) {
|
||||||
|
existingTables["cmap"] = cmap
|
||||||
|
malformedDirectory = true
|
||||||
|
}
|
||||||
missingHead := existingTables["head"] == nil
|
missingHead := existingTables["head"] == nil
|
||||||
if !missingHead {
|
if !missingHead {
|
||||||
head := existingTables["head"]
|
head := existingTables["head"]
|
||||||
|
|||||||
@@ -24,6 +24,31 @@ import (
|
|||||||
// 坐标以左上角为原点,单位为毫米,子路径从Move开始,Close回到当前子路径起点
|
// 坐标以左上角为原点,单位为毫米,子路径从Move开始,Close回到当前子路径起点
|
||||||
type GeometryPath []GeometrySegment
|
type GeometryPath []GeometrySegment
|
||||||
|
|
||||||
|
// stationaryEndpoint 判断贝塞尔曲线端点是否具有零一阶导数
|
||||||
|
// 返回: bool 是否存在驻点端点
|
||||||
|
func (p GeometryPath) stationaryEndpoint() bool {
|
||||||
|
var current, start Point
|
||||||
|
for _, segment := range p {
|
||||||
|
switch segment.Verb {
|
||||||
|
case GeometryMove:
|
||||||
|
start = segment.End
|
||||||
|
case GeometryCubic:
|
||||||
|
if segment.Control1 == current || segment.Control2 == segment.End {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case GeometryQuad:
|
||||||
|
if segment.Control1 == current || segment.Control1 == segment.End {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case GeometryClose:
|
||||||
|
current = start
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
current = segment.End
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// GeometryVerb 表示几何路径指令
|
// GeometryVerb 表示几何路径指令
|
||||||
type GeometryVerb uint8
|
type GeometryVerb uint8
|
||||||
|
|
||||||
|
|||||||
+33
-19
@@ -21,15 +21,16 @@ import (
|
|||||||
|
|
||||||
// geometryPolyline 保存展开后的子路径,独立记录闭合状态
|
// geometryPolyline 保存展开后的子路径,独立记录闭合状态
|
||||||
type geometryPolyline struct {
|
type geometryPolyline struct {
|
||||||
points []Point
|
points []Point
|
||||||
closed bool
|
closed bool
|
||||||
|
direction Point
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stroke 将曲线、正长度虚线及线帽连接转换为非零填充轮廓
|
// Stroke 将曲线、虚线及线帽连接转换为非零填充轮廓
|
||||||
// 曲线按Tolerance展开,默认误差为0.001毫米,重叠轮廓保持同向绕数
|
// 曲线按Tolerance展开,默认误差为0.001毫米,重叠轮廓保持同向绕数
|
||||||
// 结果未做布尔整理,按非零规则填充,区域运算需能处理重叠子路径的后端
|
// 结果未做布尔整理,按非零规则填充,区域运算需能处理重叠子路径的后端
|
||||||
// 入参: options 描边样式
|
// 入参: options 描边样式
|
||||||
// 返回: GeometryPath 描边轮廓, error 无效参数、不支持的零长度语义或精度限制
|
// 返回: GeometryPath 描边轮廓, error 无效参数或精度限制
|
||||||
func (p GeometryPath) Stroke(options StrokeOptions) (GeometryPath, error) {
|
func (p GeometryPath) Stroke(options StrokeOptions) (GeometryPath, error) {
|
||||||
if err := validateStroke(options); err != nil {
|
if err := validateStroke(options); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -51,11 +52,6 @@ func (p GeometryPath) Stroke(options StrokeOptions) (GeometryPath, error) {
|
|||||||
if total == 0 {
|
if total == 0 {
|
||||||
dashes = nil
|
dashes = nil
|
||||||
} else {
|
} else {
|
||||||
for _, d := range dashes {
|
|
||||||
if d == 0 {
|
|
||||||
return nil, fmt.Errorf("zero-length stroke dash: %w", ErrBackendUnavailable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(dashes)%2 != 0 {
|
if len(dashes)%2 != 0 {
|
||||||
dashes = append(dashes, dashes...)
|
dashes = append(dashes, dashes...)
|
||||||
total *= 2
|
total *= 2
|
||||||
@@ -199,7 +195,7 @@ func (p GeometryPath) strokePolylines(tolerance float64) ([]geometryPolyline, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
// dash 按子路径重置相位并合并跨闭合接缝的连续虚线
|
// dash 按子路径重置相位并合并跨闭合接缝的连续虚线
|
||||||
// 入参: pattern 正长度偶数虚线数组, total 周期, offset 偏移
|
// 入参: pattern 非负偶数虚线数组, total 周期, offset 偏移
|
||||||
// 返回: []geometryPolyline 虚线段, error 长度或细分限制
|
// 返回: []geometryPolyline 虚线段, error 长度或细分限制
|
||||||
func (line geometryPolyline) dash(pattern []float64, total, offset float64) ([]geometryPolyline, error) {
|
func (line geometryPolyline) dash(pattern []float64, total, offset float64) ([]geometryPolyline, error) {
|
||||||
phase := math.Mod(offset, total)
|
phase := math.Mod(offset, total)
|
||||||
@@ -207,7 +203,7 @@ func (line geometryPolyline) dash(pattern []float64, total, offset float64) ([]g
|
|||||||
phase += total
|
phase += total
|
||||||
}
|
}
|
||||||
index := 0
|
index := 0
|
||||||
for phase >= pattern[index] {
|
for phase > 0 && phase >= pattern[index] {
|
||||||
phase -= pattern[index]
|
phase -= pattern[index]
|
||||||
index = (index + 1) % len(pattern)
|
index = (index + 1) % len(pattern)
|
||||||
}
|
}
|
||||||
@@ -233,7 +229,24 @@ func (line geometryPolyline) dash(pattern []float64, total, offset float64) ([]g
|
|||||||
if !finite(length) {
|
if !finite(length) {
|
||||||
return nil, fmt.Errorf("stroke dash length overflow")
|
return nil, fmt.Errorf("stroke dash length overflow")
|
||||||
}
|
}
|
||||||
for at := 0.0; at < length; {
|
for at := 0.0; ; {
|
||||||
|
for remaining == 0 {
|
||||||
|
steps++
|
||||||
|
if steps > 1<<18 {
|
||||||
|
return nil, fmt.Errorf("stroke dash subdivision limit exceeded")
|
||||||
|
}
|
||||||
|
if pattern[index] == 0 && index%2 == 0 && len(run) == 0 {
|
||||||
|
result = append(result, geometryPolyline{points: []Point{geometryLerp(a, b, at/length)}, direction: Point{(b.X - a.X) / length, (b.Y - a.Y) / length}})
|
||||||
|
}
|
||||||
|
index = (index + 1) % len(pattern)
|
||||||
|
remaining = pattern[index]
|
||||||
|
if index%2 == 1 && remaining > 0 {
|
||||||
|
flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if at == length {
|
||||||
|
break
|
||||||
|
}
|
||||||
step := math.Min(remaining, length-at)
|
step := math.Min(remaining, length-at)
|
||||||
if at+step == at || steps > 1<<18 {
|
if at+step == at || steps > 1<<18 {
|
||||||
return nil, fmt.Errorf("stroke dash subdivision limit exceeded")
|
return nil, fmt.Errorf("stroke dash subdivision limit exceeded")
|
||||||
@@ -247,11 +260,6 @@ func (line geometryPolyline) dash(pattern []float64, total, offset float64) ([]g
|
|||||||
}
|
}
|
||||||
at += step
|
at += step
|
||||||
remaining -= step
|
remaining -= step
|
||||||
if remaining == 0 {
|
|
||||||
flush()
|
|
||||||
index = (index + 1) % len(pattern)
|
|
||||||
remaining = pattern[index]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
flush()
|
flush()
|
||||||
@@ -259,7 +267,7 @@ func (line geometryPolyline) dash(pattern []float64, total, offset float64) ([]g
|
|||||||
first, last := &result[0], &result[len(result)-1]
|
first, last := &result[0], &result[len(result)-1]
|
||||||
if first.points[0] == line.points[0] && last.points[len(last.points)-1] == line.points[0] {
|
if first.points[0] == line.points[0] && last.points[len(last.points)-1] == line.points[0] {
|
||||||
if len(result) == 1 {
|
if len(result) == 1 {
|
||||||
first.closed = true
|
first.closed = len(first.points) > 1
|
||||||
} else {
|
} else {
|
||||||
first.points = append(last.points[:len(last.points)-1], first.points...)
|
first.points = append(last.points[:len(last.points)-1], first.points...)
|
||||||
result = result[:len(result)-1]
|
result = result[:len(result)-1]
|
||||||
@@ -316,7 +324,13 @@ func (line geometryPolyline) stroke(options StrokeOptions) (GeometryPath, error)
|
|||||||
if options.Cap == "Round" {
|
if options.Cap == "Round" {
|
||||||
disk(points[0])
|
disk(points[0])
|
||||||
} else if options.Cap == "Square" {
|
} else if options.Cap == "Square" {
|
||||||
return nil, fmt.Errorf("directionless square stroke cap: %w", ErrBackendUnavailable)
|
d := line.direction
|
||||||
|
if d == (Point{}) {
|
||||||
|
return nil, fmt.Errorf("directionless square stroke cap: %w", ErrBackendUnavailable)
|
||||||
|
}
|
||||||
|
p := points[0]
|
||||||
|
u, v := Point{d.X * half, d.Y * half}, Point{-d.Y * half, d.X * half}
|
||||||
|
polygon(Point{p.X - u.X - v.X, p.Y - u.Y - v.Y}, Point{p.X + u.X - v.X, p.Y + u.Y - v.Y}, Point{p.X + u.X + v.X, p.Y + u.Y + v.Y}, Point{p.X - u.X + v.X, p.Y - u.Y + v.Y})
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -182,7 +182,7 @@ func (r *Renderer) parseFillColor(fillColor *FillColor) color.Color {
|
|||||||
if fillColor == nil {
|
if fillColor == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if fillColor.Pattern != nil {
|
if fillColor.Pattern != nil || fillColor.GouraudShd != nil || fillColor.LaGouraudShd != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if fillColor.AxialShd != nil {
|
if fillColor.AxialShd != nil {
|
||||||
|
|||||||
@@ -173,6 +173,31 @@ func (p *pdfImporter) compositePage(space *pdfgo.ColorSpace, walk func(pdfgo.Vis
|
|||||||
}
|
}
|
||||||
if space == nil {
|
if space == nil {
|
||||||
space = &pdfgo.ColorSpace{Model: "DeviceRGB"}
|
space = &pdfgo.ColorSpace{Model: "DeviceRGB"}
|
||||||
|
var needsProcess func([]pdfCompositeNode) (bool, error)
|
||||||
|
needsProcess = func(items []pdfCompositeNode) (bool, error) {
|
||||||
|
for _, item := range items {
|
||||||
|
if item.group != nil {
|
||||||
|
found, err := needsProcess(item.children)
|
||||||
|
if err != nil || found {
|
||||||
|
return found, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if item.image != nil && item.image.Style.FillOverprint {
|
||||||
|
_, found, err := pdfImageProcessColorants(item.image.Image)
|
||||||
|
if err != nil || found {
|
||||||
|
return found, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
found, err := needsProcess(nodes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if found {
|
||||||
|
space = &pdfgo.ColorSpace{Model: "DeviceCMYK"}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for i, node := range nodes {
|
for i, node := range nodes {
|
||||||
if node.opaque(space) || space.SRGBEquivalent() && node.direct() {
|
if node.opaque(space) || space.SRGBEquivalent() && node.direct() {
|
||||||
@@ -211,6 +236,9 @@ func (n pdfCompositeNode) opaque(space *pdfgo.ColorSpace) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
style, fill, stroke := n.style()
|
style, fill, stroke := n.style()
|
||||||
|
if n.image != nil && style.FillOverprint {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if style.SoftMask != nil || !pdfNormalBlend(style.BlendMode) || fill && style.Fill.Alpha != 1 || stroke && style.Stroke.Alpha != 1 {
|
if style.SoftMask != nil || !pdfNormalBlend(style.BlendMode) || fill && style.Fill.Alpha != 1 || stroke && style.Stroke.Alpha != 1 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -241,6 +269,9 @@ func (n pdfCompositeNode) direct() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
s, fill, stroke := n.style()
|
s, fill, stroke := n.style()
|
||||||
|
if n.image != nil && s.FillOverprint {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return pdfNormalBlend(s.BlendMode) && !(fill && s.FillOverprint && pdfOverprintNeedsSeparation(s.Fill) || stroke && s.StrokeOverprint && pdfOverprintNeedsSeparation(s.Stroke))
|
return pdfNormalBlend(s.BlendMode) && !(fill && s.FillOverprint && pdfOverprintNeedsSeparation(s.Fill) || stroke && s.StrokeOverprint && pdfOverprintNeedsSeparation(s.Stroke))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,14 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
style, _, _ := node.style()
|
style, _, _ := node.style()
|
||||||
|
var processMask [4]bool
|
||||||
|
processOverprint := false
|
||||||
|
if node.image != nil && overprint && space.Model == "DeviceCMYK" && !space.Calibrated() {
|
||||||
|
processMask, processOverprint, err = pdfImageProcessColorants(node.image.Image)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
compatible := overprint && style.OverprintMode == 1 && node.image == nil && space.Model == "DeviceCMYK" && !space.Calibrated() && (paint.CMYK != nil || paint.Space != nil && paint.Space.Model == "DeviceCMYK" && !paint.Space.Calibrated())
|
compatible := overprint && style.OverprintMode == 1 && node.image == nil && space.Model == "DeviceCMYK" && !space.Calibrated() && (paint.CMYK != nil || paint.Space != nil && paint.Space.Model == "DeviceCMYK" && !paint.Space.Calibrated())
|
||||||
step := 25.4 / c.importer.rasterDPI
|
step := 25.4 / c.importer.rasterDPI
|
||||||
uniform := node.image == nil && paint.Axial == nil && paint.Radial == nil && paint.Tiling == nil && paint.Mesh == nil
|
uniform := node.image == nil && paint.Axial == nil && paint.Radial == nil && paint.Tiling == nil && paint.Mesh == nil
|
||||||
@@ -156,12 +164,28 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
|||||||
}
|
}
|
||||||
if initial != nil {
|
if initial != nil {
|
||||||
pixel := initial[index]
|
pixel := initial[index]
|
||||||
|
if processOverprint {
|
||||||
|
for c := range values {
|
||||||
|
if !processMask[c] {
|
||||||
|
values[c] = pixel.values[c] * pixel.alpha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := pdfCompositePaintOver(&pixel, values, opacity, space, mode, compatible); err != nil {
|
if err := pdfCompositePaintOver(&pixel, values, opacity, space, mode, compatible); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
pdfCompositeInterpolate(&pixels[index], pixel, shape)
|
pdfCompositeInterpolate(&pixels[index], pixel, shape)
|
||||||
} else if err := pdfCompositePaintOver(&pixels[index], values, shape*opacity, space, mode, compatible); err != nil {
|
} else {
|
||||||
return err
|
if processOverprint {
|
||||||
|
for c := range values {
|
||||||
|
if !processMask[c] {
|
||||||
|
values[c] = pixels[index].values[c] * pixels[index].alpha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := pdfCompositePaintOver(&pixels[index], values, shape*opacity, space, mode, compatible); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pixels[index].shape += shape * (1 - pixels[index].shape)
|
pixels[index].shape += shape * (1 - pixels[index].shape)
|
||||||
}
|
}
|
||||||
@@ -359,8 +383,33 @@ func (c *pdfCompositor) imageColor(mark pdfgo.ImageMark, point pdfgo.Point, spac
|
|||||||
return [4]float64{}, 0, nil
|
return [4]float64{}, 0, nil
|
||||||
}
|
}
|
||||||
x, y := point.X*float64(source.Rect.Dx())-.5, (1-point.Y)*float64(source.Rect.Dy())-.5
|
x, y := point.X*float64(source.Rect.Dx())-.5, (1-point.Y)*float64(source.Rect.Dy())-.5
|
||||||
|
process := mark.Style.FillOverprint && space.Model == "DeviceCMYK" && !space.Calibrated() && len(source.Colorants) > 0
|
||||||
|
var indices [4]int
|
||||||
|
if process {
|
||||||
|
for i, name := range source.Colorants {
|
||||||
|
index := -1
|
||||||
|
for j, known := range []pdfgo.Name{"Cyan", "Magenta", "Yellow", "Black"} {
|
||||||
|
if name == known {
|
||||||
|
index = j
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if index < 0 {
|
||||||
|
process = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
indices[i] = index
|
||||||
|
}
|
||||||
|
}
|
||||||
sample := func(x, y int) ([4]float64, float64) {
|
sample := func(x, y int) ([4]float64, float64) {
|
||||||
return source.ValuesAt(max(0, min(source.Rect.Dx()-1, x)), max(0, min(source.Rect.Dy()-1, y)))
|
x, y = max(0, min(source.Rect.Dx()-1, x)), max(0, min(source.Rect.Dy()-1, y))
|
||||||
|
values, alpha := source.ValuesAt(x, y)
|
||||||
|
if process {
|
||||||
|
values = [4]float64{}
|
||||||
|
for j, tint := range source.TintsAt(x, y) {
|
||||||
|
values[indices[j]] = float64(tint) / 65535
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values, alpha
|
||||||
}
|
}
|
||||||
values, alpha := sample(int(math.Floor(x+.5)), int(math.Floor(y+.5)))
|
values, alpha := sample(int(math.Floor(x+.5)), int(math.Floor(y+.5)))
|
||||||
if mark.Image.Interpolate {
|
if mark.Image.Interpolate {
|
||||||
@@ -381,7 +430,7 @@ func (c *pdfCompositor) imageColor(mark pdfgo.ImageMark, point pdfgo.Point, spac
|
|||||||
}
|
}
|
||||||
v, a := sample(ix+i, iy+j)
|
v, a := sample(ix+i, iy+j)
|
||||||
alpha += a * weight
|
alpha += a * weight
|
||||||
for k := 0; k < source.Space.Components(); k++ {
|
for k := range values {
|
||||||
values[k] += v[k] * a * weight
|
values[k] += v[k] * a * weight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -399,10 +448,41 @@ func (c *pdfCompositor) imageColor(mark pdfgo.ImageMark, point pdfgo.Point, spac
|
|||||||
}
|
}
|
||||||
return color, alpha * (1 - values[0]), err
|
return color, alpha * (1 - values[0]), err
|
||||||
}
|
}
|
||||||
|
if process {
|
||||||
|
return values, alpha, nil
|
||||||
|
}
|
||||||
values, err := space.Convert(values[:source.Space.Components()], source.Space, mark.Style.RenderingIntent)
|
values, err := space.Convert(values[:source.Space.Components()], source.Space, mark.Style.RenderingIntent)
|
||||||
return values, alpha, err
|
return values, alpha, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pdfImageProcessColorants 识别可在四色设备上直接绘制的专色色料
|
||||||
|
// 入参: image PDF图像
|
||||||
|
// 返回: [4]bool 参与绘制的四色通道, bool 是否全部属于印刷原色, error 色空间错误
|
||||||
|
func pdfImageProcessColorants(image *pdfgo.Image) ([4]bool, bool, error) {
|
||||||
|
var mask [4]bool
|
||||||
|
names, err := image.Colorants()
|
||||||
|
if err != nil {
|
||||||
|
return mask, false, err
|
||||||
|
}
|
||||||
|
if len(names) == 0 {
|
||||||
|
return mask, false, nil
|
||||||
|
}
|
||||||
|
for _, name := range names {
|
||||||
|
found := false
|
||||||
|
for j, known := range []pdfgo.Name{"Cyan", "Magenta", "Yellow", "Black"} {
|
||||||
|
if name == known {
|
||||||
|
mask[j] = true
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return [4]bool{}, false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mask, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
// pdfCompositeColor 将原始画刷变换到混合空间,渐变在源空间求值
|
// pdfCompositeColor 将原始画刷变换到混合空间,渐变在源空间求值
|
||||||
// 入参: paint 画刷, point 页面坐标, space 混合空间, intent 渲染意图
|
// 入参: paint 画刷, point 页面坐标, space 混合空间, intent 渲染意图
|
||||||
// 返回: [4]float64 混合分量, bool 是否着色, error 颜色错误
|
// 返回: [4]float64 混合分量, bool 是否着色, error 颜色错误
|
||||||
|
|||||||
+18
-1
@@ -473,7 +473,21 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, uint16, er
|
|||||||
tables["hmtx"] = tables["hmtx"][:metricLength]
|
tables["hmtx"] = tables["hmtx"][:metricLength]
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
if len(tables["cmap"]) == 0 || source.Subtype == "Type0" && pdfCmapOverlaps(tables["cmap"]) {
|
invalidCmap := false
|
||||||
|
if source.Subtype == "TrueType" || source.Subtype == "Type0" {
|
||||||
|
mapping := parseCmapMappings(tables["cmap"])
|
||||||
|
for char, glyph := range mapping {
|
||||||
|
if glyph >= count {
|
||||||
|
delete(mapping, char)
|
||||||
|
invalidCmap = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if invalidCmap && source.Subtype == "TrueType" {
|
||||||
|
tables["cmap"] = buildCmapTable(count, mapping)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(tables["cmap"]) == 0 || source.Subtype == "Type0" && (invalidCmap || pdfCmapOverlaps(tables["cmap"])) {
|
||||||
mapping := map[rune]uint16{}
|
mapping := map[rune]uint16{}
|
||||||
for _, code := range slices.Sorted(maps.Keys(source.Unicode)) {
|
for _, code := range slices.Sorted(maps.Keys(source.Unicode)) {
|
||||||
glyphs, err := source.Decode([]byte(code))
|
glyphs, err := source.Decode([]byte(code))
|
||||||
@@ -484,6 +498,9 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, uint16, er
|
|||||||
return nil, 0, &pdfgo.UnsupportedError{Feature: "font without explicit glyph mapping"}
|
return nil, 0, &pdfgo.UnsupportedError{Feature: "font without explicit glyph mapping"}
|
||||||
}
|
}
|
||||||
glyph := glyphs[0]
|
glyph := glyphs[0]
|
||||||
|
if glyph.ID >= count {
|
||||||
|
return nil, 0, fmt.Errorf("PDF font glyph %d exceeds glyph count %d", glyph.ID, count)
|
||||||
|
}
|
||||||
if utf8.RuneCountInString(glyph.Text) == 1 {
|
if utf8.RuneCountInString(glyph.Text) == 1 {
|
||||||
char, _ := utf8.DecodeRuneInString(glyph.Text)
|
char, _ := utf8.DecodeRuneInString(glyph.Text)
|
||||||
if _, exists := mapping[char]; !exists {
|
if _, exists := mapping[char]; !exists {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"image/draw"
|
"image/draw"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
"github.com/tdewolff/canvas/renderers/rasterizer"
|
"github.com/tdewolff/canvas/renderers/rasterizer"
|
||||||
@@ -148,6 +149,14 @@ func (b CanvasBackend) Render(page *RasterPage) (image.Image, error) {
|
|||||||
style.Dashes[i] = dash / options.Width
|
style.Dashes[i] = dash / options.Width
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if slices.Contains(options.Dashes, 0) {
|
||||||
|
var err error
|
||||||
|
p, err = canvasStrokePath(b, p, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
style = canvas.Style{Fill: paint, FillRule: canvas.NonZero}
|
||||||
|
}
|
||||||
m = RasterMatrix{1, 0, 0, 1, 0, 0}
|
m = RasterMatrix{1, 0, 0, 1, 0, 0}
|
||||||
}
|
}
|
||||||
matrix := canvas.Matrix{{m[0], m[2], m[4]}, {-m[1], -m[3], page.Height - m[5]}}
|
matrix := canvas.Matrix{{m[0], m[2], m[4]}, {-m[1], -m[3], page.Height - m[5]}}
|
||||||
|
|||||||
@@ -70,12 +70,12 @@ func (c *canvasPageCompiler) RenderPath(path *canvas.Path, style canvas.Style, m
|
|||||||
}
|
}
|
||||||
if style.HasStroke() {
|
if style.HasStroke() {
|
||||||
stroke := path
|
stroke := path
|
||||||
|
options := canvasStrokeOptions(style.StrokeWidth, style.StrokeCapper, style.StrokeJoiner, canvas.PixelTolerance/(c.page.DPI/25.4))
|
||||||
if len(style.Dashes) > 0 {
|
if len(style.Dashes) > 0 {
|
||||||
offset, dashes := canvas.ScaleDash(style.StrokeWidth, style.DashOffset, style.Dashes)
|
options.DashOffset, options.Dashes = canvas.ScaleDash(style.StrokeWidth, style.DashOffset, style.Dashes)
|
||||||
stroke = stroke.Dash(offset, dashes...)
|
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
stroke, err = canvasStrokePath(c.geometry, stroke, canvasStrokeOptions(style.StrokeWidth, style.StrokeCapper, style.StrokeJoiner, canvas.PixelTolerance/(c.page.DPI/25.4)))
|
stroke, err = canvasStrokePath(c.geometry, stroke, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.err = err
|
c.err = err
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
@@ -77,12 +78,21 @@ func canvasStrokePath(geometry GeometryBackend, path *canvas.Path, options Strok
|
|||||||
// 入参: path Canvas路径, width 描边宽度, cap 线帽, join 连接
|
// 入参: path Canvas路径, width 描边宽度, cap 线帽, join 连接
|
||||||
// 返回: *canvas.Path 描边轮廓
|
// 返回: *canvas.Path 描边轮廓
|
||||||
func (r *Renderer) strokeCanvasPath(path *canvas.Path, width float64, cap canvas.Capper, join canvas.Joiner) *canvas.Path {
|
func (r *Renderer) strokeCanvasPath(path *canvas.Path, width float64, cap canvas.Capper, join canvas.Joiner) *canvas.Path {
|
||||||
|
return r.strokeDashedCanvasPath(path, width, cap, join, 0, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// strokeDashedCanvasPath 通过公共几何后端展开虚线和线帽,保留零长度绘制段
|
||||||
|
// 入参: path 路径, width 线宽, cap 线帽, join 连接, offset 虚线偏移, dashes 虚线数组
|
||||||
|
// 返回: *canvas.Path 描边填充轮廓
|
||||||
|
func (r *Renderer) strokeDashedCanvasPath(path *canvas.Path, width float64, cap canvas.Capper, join canvas.Joiner, offset float64, dashes []float64) *canvas.Path {
|
||||||
geometry, err := r.Geometry()
|
geometry, err := r.Geometry()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.renderError = err
|
r.renderError = err
|
||||||
return &canvas.Path{}
|
return &canvas.Path{}
|
||||||
}
|
}
|
||||||
result, err := canvasStrokePath(geometry, path, canvasStrokeOptions(width, cap, join, canvas.Tolerance))
|
options := canvasStrokeOptions(width, cap, join, canvas.Tolerance)
|
||||||
|
options.DashOffset, options.Dashes = offset, dashes
|
||||||
|
result, err := canvasStrokePath(geometry, path, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.renderError = err
|
r.renderError = err
|
||||||
return &canvas.Path{}
|
return &canvas.Path{}
|
||||||
@@ -191,13 +201,16 @@ func (CanvasBackend) Combine(left, right GeometryPath, operation GeometryOperati
|
|||||||
return *geometryFromCanvasPath(result), nil
|
return *geometryFromCanvasPath(result), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stroke 将绝对长度虚线与描边转换为填充区域
|
// Stroke 将绝对长度虚线与描边转换为填充区域,驻点曲线按指定精度展开
|
||||||
// 入参: path 页面路径, options 描边样式
|
// 入参: path 页面路径, options 描边样式
|
||||||
// 返回: GeometryPath 描边区域, error 样式或路径错误
|
// 返回: GeometryPath 描边区域, error 样式或路径错误
|
||||||
func (CanvasBackend) Stroke(path GeometryPath, options StrokeOptions) (GeometryPath, error) {
|
func (CanvasBackend) Stroke(path GeometryPath, options StrokeOptions) (GeometryPath, error) {
|
||||||
if err := validateStroke(options); err != nil {
|
if err := validateStroke(options); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if slices.Contains(options.Dashes, 0) {
|
||||||
|
return path.Stroke(options)
|
||||||
|
}
|
||||||
p, err := geometryToCanvasPath(&path)
|
p, err := geometryToCanvasPath(&path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -209,7 +222,11 @@ func (CanvasBackend) Stroke(path GeometryPath, options StrokeOptions) (GeometryP
|
|||||||
style := pathStyle{lineJoin: canvas.MiterJoin, lineCap: canvas.ButtCap, miterLimit: defaultMiterLimit}
|
style := pathStyle{lineJoin: canvas.MiterJoin, lineCap: canvas.ButtCap, miterLimit: defaultMiterLimit}
|
||||||
style.applyLineJoin(options.Join, options.MiterLimit)
|
style.applyLineJoin(options.Join, options.MiterLimit)
|
||||||
style.lineCap = pathLineCap(options.Cap, style.lineCap)
|
style.lineCap = pathLineCap(options.Cap, style.lineCap)
|
||||||
p = p.Dash(options.DashOffset, options.Dashes...).Stroke(options.Width, style.lineCap, style.lineJoin, tolerance)
|
p = p.Dash(options.DashOffset, options.Dashes...)
|
||||||
|
if path.stationaryEndpoint() {
|
||||||
|
p = p.Flatten(tolerance)
|
||||||
|
}
|
||||||
|
p = p.Stroke(options.Width, style.lineCap, style.lineJoin, tolerance)
|
||||||
return *geometryFromCanvasPath(p), nil
|
return *geometryFromCanvasPath(p), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ package ofdgo
|
|||||||
import (
|
import (
|
||||||
"image/color"
|
"image/color"
|
||||||
"math"
|
"math"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
)
|
)
|
||||||
@@ -291,15 +292,20 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
|
|||||||
ctx.SetStrokeCapper(style.lineCap)
|
ctx.SetStrokeCapper(style.lineCap)
|
||||||
ctx.SetStrokeJoiner(style.lineJoin)
|
ctx.SetStrokeJoiner(style.lineJoin)
|
||||||
sp := p
|
sp := p
|
||||||
if len(style.dashPattern) > 0 {
|
zeroDash := slices.Contains(style.dashPattern, 0)
|
||||||
|
if len(style.dashPattern) > 0 && !zeroDash {
|
||||||
sp = sp.Dash(style.dashOffset, style.dashPattern...)
|
sp = sp.Dash(style.dashOffset, style.dashPattern...)
|
||||||
ctx.SetDashes(0)
|
ctx.SetDashes(0)
|
||||||
}
|
}
|
||||||
strokeClip := intersectClipPath(clipPath, shadingClip)
|
strokeClip := intersectClipPath(clipPath, shadingClip)
|
||||||
_, repeat := strokePaint.(*repeatAxialGradient)
|
_, repeat := strokePaint.(*repeatAxialGradient)
|
||||||
if strokeClip != nil || strokeView != canvas.Identity || repeat || style.strokePattern != nil || !canvasNativeStroke(r.backends.Geometry) {
|
if strokeClip != nil || strokeView != canvas.Identity || repeat || style.strokePattern != nil || !canvasNativeStroke(r.backends.Geometry) || zeroDash {
|
||||||
sp = sp.Copy()
|
sp = sp.Copy()
|
||||||
sp = r.strokeCanvasPath(sp, style.lineWidth, style.lineCap, style.lineJoin)
|
if zeroDash {
|
||||||
|
sp = r.strokeDashedCanvasPath(sp, style.lineWidth, style.lineCap, style.lineJoin, style.dashOffset, style.dashPattern)
|
||||||
|
} else {
|
||||||
|
sp = r.strokeCanvasPath(sp, style.lineWidth, style.lineCap, style.lineJoin)
|
||||||
|
}
|
||||||
sp = applyClipPath(sp, strokeClip)
|
sp = applyClipPath(sp, strokeClip)
|
||||||
if style.strokePattern != nil {
|
if style.strokePattern != nil {
|
||||||
r.renderPattern(ctx, style.strokePattern, pageH, sp, objectCTM)
|
r.renderPattern(ctx, style.strokePattern, pageH, sp, objectCTM)
|
||||||
|
|||||||
@@ -295,10 +295,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
|||||||
ctx.SetStrokeColor(canvas.Transparent)
|
ctx.SetStrokeColor(canvas.Transparent)
|
||||||
r.drawShdPath(ctx, applyClipPath(path.Copy(), fillClip), fillView)
|
r.drawShdPath(ctx, applyClipPath(path.Copy(), fillClip), fillView)
|
||||||
}
|
}
|
||||||
if len(strokeStyle.dashPattern) > 0 {
|
path = r.strokeDashedCanvasPath(path, strokeStyle.lineWidth, strokeStyle.lineCap, strokeStyle.lineJoin, strokeStyle.dashOffset, strokeStyle.dashPattern)
|
||||||
path = path.Dash(strokeStyle.dashOffset, strokeStyle.dashPattern...)
|
|
||||||
}
|
|
||||||
path = r.strokeCanvasPath(path, strokeStyle.lineWidth, strokeStyle.lineCap, strokeStyle.lineJoin)
|
|
||||||
path = applyClipPath(path, strokeClip)
|
path = applyClipPath(path, strokeClip)
|
||||||
if strokeStyle.strokePattern != nil {
|
if strokeStyle.strokePattern != nil {
|
||||||
r.renderPattern(ctx, strokeStyle.strokePattern, pageH, path, objectCTM)
|
r.renderPattern(ctx, strokeStyle.strokePattern, pageH, path, objectCTM)
|
||||||
|
|||||||
Reference in new issue
Block a user