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/font v0.0.0-20260913163313-54f98bb59ee6
|
||||
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/image v0.46.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/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
|
||||
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-20260927050705-f09ba3f6aa16/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
|
||||
github.com/xiaoqidun/pdfgo v0.0.0-20260927070002-9eedb7bf8e13 h1:P3UEKffubpK6Cqh1ulQysufcw3c+zhEsUv4z+EhqCDA=
|
||||
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/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
@@ -53,6 +54,14 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin
|
||||
if existingTables["CFF "] != nil {
|
||||
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
|
||||
if !missingHead {
|
||||
head := existingTables["head"]
|
||||
|
||||
@@ -24,6 +24,31 @@ import (
|
||||
// 坐标以左上角为原点,单位为毫米,子路径从Move开始,Close回到当前子路径起点
|
||||
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 表示几何路径指令
|
||||
type GeometryVerb uint8
|
||||
|
||||
|
||||
+33
-19
@@ -21,15 +21,16 @@ import (
|
||||
|
||||
// geometryPolyline 保存展开后的子路径,独立记录闭合状态
|
||||
type geometryPolyline struct {
|
||||
points []Point
|
||||
closed bool
|
||||
points []Point
|
||||
closed bool
|
||||
direction Point
|
||||
}
|
||||
|
||||
// Stroke 将曲线、正长度虚线及线帽连接转换为非零填充轮廓
|
||||
// Stroke 将曲线、虚线及线帽连接转换为非零填充轮廓
|
||||
// 曲线按Tolerance展开,默认误差为0.001毫米,重叠轮廓保持同向绕数
|
||||
// 结果未做布尔整理,按非零规则填充,区域运算需能处理重叠子路径的后端
|
||||
// 入参: options 描边样式
|
||||
// 返回: GeometryPath 描边轮廓, error 无效参数、不支持的零长度语义或精度限制
|
||||
// 返回: GeometryPath 描边轮廓, error 无效参数或精度限制
|
||||
func (p GeometryPath) Stroke(options StrokeOptions) (GeometryPath, error) {
|
||||
if err := validateStroke(options); err != nil {
|
||||
return nil, err
|
||||
@@ -51,11 +52,6 @@ func (p GeometryPath) Stroke(options StrokeOptions) (GeometryPath, error) {
|
||||
if total == 0 {
|
||||
dashes = nil
|
||||
} else {
|
||||
for _, d := range dashes {
|
||||
if d == 0 {
|
||||
return nil, fmt.Errorf("zero-length stroke dash: %w", ErrBackendUnavailable)
|
||||
}
|
||||
}
|
||||
if len(dashes)%2 != 0 {
|
||||
dashes = append(dashes, dashes...)
|
||||
total *= 2
|
||||
@@ -199,7 +195,7 @@ func (p GeometryPath) strokePolylines(tolerance float64) ([]geometryPolyline, er
|
||||
}
|
||||
|
||||
// dash 按子路径重置相位并合并跨闭合接缝的连续虚线
|
||||
// 入参: pattern 正长度偶数虚线数组, total 周期, offset 偏移
|
||||
// 入参: pattern 非负偶数虚线数组, total 周期, offset 偏移
|
||||
// 返回: []geometryPolyline 虚线段, error 长度或细分限制
|
||||
func (line geometryPolyline) dash(pattern []float64, total, offset float64) ([]geometryPolyline, error) {
|
||||
phase := math.Mod(offset, total)
|
||||
@@ -207,7 +203,7 @@ func (line geometryPolyline) dash(pattern []float64, total, offset float64) ([]g
|
||||
phase += total
|
||||
}
|
||||
index := 0
|
||||
for phase >= pattern[index] {
|
||||
for phase > 0 && phase >= pattern[index] {
|
||||
phase -= pattern[index]
|
||||
index = (index + 1) % len(pattern)
|
||||
}
|
||||
@@ -233,7 +229,24 @@ func (line geometryPolyline) dash(pattern []float64, total, offset float64) ([]g
|
||||
if !finite(length) {
|
||||
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)
|
||||
if at+step == at || steps > 1<<18 {
|
||||
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
|
||||
remaining -= step
|
||||
if remaining == 0 {
|
||||
flush()
|
||||
index = (index + 1) % len(pattern)
|
||||
remaining = pattern[index]
|
||||
}
|
||||
}
|
||||
}
|
||||
flush()
|
||||
@@ -259,7 +267,7 @@ func (line geometryPolyline) dash(pattern []float64, total, offset float64) ([]g
|
||||
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 len(result) == 1 {
|
||||
first.closed = true
|
||||
first.closed = len(first.points) > 1
|
||||
} else {
|
||||
first.points = append(last.points[:len(last.points)-1], first.points...)
|
||||
result = result[:len(result)-1]
|
||||
@@ -316,7 +324,13 @@ func (line geometryPolyline) stroke(options StrokeOptions) (GeometryPath, error)
|
||||
if options.Cap == "Round" {
|
||||
disk(points[0])
|
||||
} 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
|
||||
}
|
||||
|
||||
+1
-1
@@ -182,7 +182,7 @@ func (r *Renderer) parseFillColor(fillColor *FillColor) color.Color {
|
||||
if fillColor == nil {
|
||||
return nil
|
||||
}
|
||||
if fillColor.Pattern != nil {
|
||||
if fillColor.Pattern != nil || fillColor.GouraudShd != nil || fillColor.LaGouraudShd != nil {
|
||||
return nil
|
||||
}
|
||||
if fillColor.AxialShd != nil {
|
||||
|
||||
@@ -173,6 +173,31 @@ func (p *pdfImporter) compositePage(space *pdfgo.ColorSpace, walk func(pdfgo.Vis
|
||||
}
|
||||
if space == nil {
|
||||
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 {
|
||||
if node.opaque(space) || space.SRGBEquivalent() && node.direct() {
|
||||
@@ -211,6 +236,9 @@ func (n pdfCompositeNode) opaque(space *pdfgo.ColorSpace) bool {
|
||||
return true
|
||||
}
|
||||
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 {
|
||||
return false
|
||||
}
|
||||
@@ -241,6 +269,9 @@ func (n pdfCompositeNode) direct() bool {
|
||||
return true
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,14 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
||||
return err
|
||||
}
|
||||
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())
|
||||
step := 25.4 / c.importer.rasterDPI
|
||||
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 {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
pdfCompositeInterpolate(&pixels[index], pixel, shape)
|
||||
} else if err := pdfCompositePaintOver(&pixels[index], values, shape*opacity, space, mode, compatible); err != nil {
|
||||
return err
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
@@ -359,8 +383,33 @@ func (c *pdfCompositor) imageColor(mark pdfgo.ImageMark, point pdfgo.Point, spac
|
||||
return [4]float64{}, 0, nil
|
||||
}
|
||||
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) {
|
||||
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)))
|
||||
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)
|
||||
alpha += a * weight
|
||||
for k := 0; k < source.Space.Components(); k++ {
|
||||
for k := range values {
|
||||
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
|
||||
}
|
||||
if process {
|
||||
return values, alpha, nil
|
||||
}
|
||||
values, err := space.Convert(values[:source.Space.Components()], source.Space, mark.Style.RenderingIntent)
|
||||
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 将原始画刷变换到混合空间,渐变在源空间求值
|
||||
// 入参: paint 画刷, point 页面坐标, space 混合空间, intent 渲染意图
|
||||
// 返回: [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]
|
||||
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{}
|
||||
for _, code := range slices.Sorted(maps.Keys(source.Unicode)) {
|
||||
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"}
|
||||
}
|
||||
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 {
|
||||
char, _ := utf8.DecodeRuneInString(glyph.Text)
|
||||
if _, exists := mapping[char]; !exists {
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"slices"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
"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
|
||||
}
|
||||
}
|
||||
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}
|
||||
}
|
||||
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() {
|
||||
stroke := path
|
||||
options := canvasStrokeOptions(style.StrokeWidth, style.StrokeCapper, style.StrokeJoiner, canvas.PixelTolerance/(c.page.DPI/25.4))
|
||||
if len(style.Dashes) > 0 {
|
||||
offset, dashes := canvas.ScaleDash(style.StrokeWidth, style.DashOffset, style.Dashes)
|
||||
stroke = stroke.Dash(offset, dashes...)
|
||||
options.DashOffset, options.Dashes = canvas.ScaleDash(style.StrokeWidth, style.DashOffset, style.Dashes)
|
||||
}
|
||||
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 {
|
||||
c.err = err
|
||||
return
|
||||
|
||||
@@ -16,6 +16,7 @@ package ofdgo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
@@ -77,12 +78,21 @@ func canvasStrokePath(geometry GeometryBackend, path *canvas.Path, options Strok
|
||||
// 入参: path Canvas路径, width 描边宽度, cap 线帽, join 连接
|
||||
// 返回: *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()
|
||||
if err != nil {
|
||||
r.renderError = err
|
||||
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 {
|
||||
r.renderError = err
|
||||
return &canvas.Path{}
|
||||
@@ -191,13 +201,16 @@ func (CanvasBackend) Combine(left, right GeometryPath, operation GeometryOperati
|
||||
return *geometryFromCanvasPath(result), nil
|
||||
}
|
||||
|
||||
// Stroke 将绝对长度虚线与描边转换为填充区域
|
||||
// Stroke 将绝对长度虚线与描边转换为填充区域,驻点曲线按指定精度展开
|
||||
// 入参: path 页面路径, options 描边样式
|
||||
// 返回: GeometryPath 描边区域, error 样式或路径错误
|
||||
func (CanvasBackend) Stroke(path GeometryPath, options StrokeOptions) (GeometryPath, error) {
|
||||
if err := validateStroke(options); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if slices.Contains(options.Dashes, 0) {
|
||||
return path.Stroke(options)
|
||||
}
|
||||
p, err := geometryToCanvasPath(&path)
|
||||
if err != nil {
|
||||
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.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)
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ package ofdgo
|
||||
import (
|
||||
"image/color"
|
||||
"math"
|
||||
"slices"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
@@ -291,15 +292,20 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
|
||||
ctx.SetStrokeCapper(style.lineCap)
|
||||
ctx.SetStrokeJoiner(style.lineJoin)
|
||||
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...)
|
||||
ctx.SetDashes(0)
|
||||
}
|
||||
strokeClip := intersectClipPath(clipPath, shadingClip)
|
||||
_, 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 = 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)
|
||||
if style.strokePattern != nil {
|
||||
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)
|
||||
r.drawShdPath(ctx, applyClipPath(path.Copy(), fillClip), fillView)
|
||||
}
|
||||
if len(strokeStyle.dashPattern) > 0 {
|
||||
path = path.Dash(strokeStyle.dashOffset, strokeStyle.dashPattern...)
|
||||
}
|
||||
path = r.strokeCanvasPath(path, strokeStyle.lineWidth, strokeStyle.lineCap, strokeStyle.lineJoin)
|
||||
path = r.strokeDashedCanvasPath(path, strokeStyle.lineWidth, strokeStyle.lineCap, strokeStyle.lineJoin, strokeStyle.dashOffset, strokeStyle.dashPattern)
|
||||
path = applyClipPath(path, strokeClip)
|
||||
if strokeStyle.strokePattern != nil {
|
||||
r.renderPattern(ctx, strokeStyle.strokePattern, pageH, path, objectCTM)
|
||||
|
||||
Reference in new issue
Block a user