feat(转换能力): 增强PDF字体与裁剪转换
build.yaml / build (push) Successful in 41s

This commit is contained in:
xiaoqidun committed 2026-09-26 08:59:54 +08:00
1 parent c64bc9d594
commit 35631cdf98
13 files changed
+1188 -76

No files matched your search

+2 -1
View File
@@ -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-20260925114744-07854fb7628b github.com/xiaoqidun/pdfgo v0.0.0-20260926005801-0f4b07fb311c
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
@@ -26,6 +26,7 @@ require (
github.com/benoitkugler/textlayout v0.3.2 // indirect github.com/benoitkugler/textlayout v0.3.2 // indirect
github.com/benoitkugler/textprocessing v0.0.6 // indirect github.com/benoitkugler/textprocessing v0.0.6 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/mububoki/jpeg2000 v1.0.0 // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388 // indirect github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388 // indirect
github.com/tdewolff/minify/v2 v2.24.17 // indirect github.com/tdewolff/minify/v2 v2.24.17 // indirect
+4 -2
View File
@@ -38,6 +38,8 @@ github.com/go-text/typesetting-utils v0.0.0-20260419141703-4ffe8874dabc h1:8FGo2
github.com/go-text/typesetting-utils v0.0.0-20260419141703-4ffe8874dabc/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o= github.com/go-text/typesetting-utils v0.0.0-20260419141703-4ffe8874dabc/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/mububoki/jpeg2000 v1.0.0 h1:1ha5mqCg0DMK6zbbBp9bkUjEds4HYwQqmU3ItF3k9qs=
github.com/mububoki/jpeg2000 v1.0.0/go.mod h1:q+kfbMgsqawC9I10Zk+goSZ8+ac4cOzcWdIL1vpsDLs=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE= github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
@@ -60,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-20260925114744-07854fb7628b h1:4j0AxYMyWpmZLcISwFG4wbNbyRxfb8wDi+j9KlGLybQ= github.com/xiaoqidun/pdfgo v0.0.0-20260926005801-0f4b07fb311c h1:ArBGxLqZghXkkIDtz2UqpsxXPM68qs3qVUqOvW9SkzM=
github.com/xiaoqidun/pdfgo v0.0.0-20260925114744-07854fb7628b/go.mod h1:/D0XHgfZ7xNhix95swpNaThpuqoJjVR5wIadogtR5pI= github.com/xiaoqidun/pdfgo v0.0.0-20260926005801-0f4b07fb311c/go.mod h1:VnsFG68jDBBkMvQePZTfAyVPq1ykFK5gsExsjVSEhmM=
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=
+11 -3
View File
@@ -767,7 +767,7 @@ func (e *Editor) prepareObject(id string, object GraphicObject) (GraphicObject,
return cloneEditorObject(object) return cloneEditorObject(object)
} }
// validateObjectClips 校验对象的路径裁剪,仅接受无资源引用及嵌套裁剪的路径 // validateObjectClips 校验对象的路径与文字裁剪,拒绝嵌套裁剪和无效资源
// 入参: clips 裁剪集合,nil表示无裁剪 // 入参: clips 裁剪集合,nil表示无裁剪
// 返回: error 错误信息 // 返回: error 错误信息
func (e *Editor) validateObjectClips(clips *Clips) error { func (e *Editor) validateObjectClips(clips *Clips) error {
@@ -782,8 +782,8 @@ func (e *Editor) validateObjectClips(clips *Clips) error {
return fmt.Errorf("clip must contain an area") return fmt.Errorf("clip must contain an area")
} }
for _, area := range clip.Area { for _, area := range clip.Area {
if len(area.Path) == 0 || area.DrawParam != "" || len(area.Text) != 0 { if len(area.Path)+len(area.Text) == 0 || area.DrawParam != "" {
return fmt.Errorf("clips only support paths without draw parameter references") return fmt.Errorf("clip area requires paths or text without draw parameter references")
} }
if area.CTM != "" { if area.CTM != "" {
if _, err := creationNumbers(area.CTM, 6); err != nil { if _, err := creationNumbers(area.CTM, 6); err != nil {
@@ -798,6 +798,14 @@ func (e *Editor) validateObjectClips(clips *Clips) error {
return err return err
} }
} }
for _, text := range area.Text {
if text.ID != "" || text.Clips != nil || text.Actions != nil {
return fmt.Errorf("clip text must not have object IDs, actions or nested clips")
}
if _, err := e.prepareObject("", GraphicObject{Type: "TextObject", TextObject: text}); err != nil {
return err
}
}
} }
} }
return nil return nil
+19 -1
View File
@@ -302,23 +302,41 @@ func collectPaintReferences(paint *FillColor, used map[string]bool) {
// collectObjectReferences 收集对象和嵌套绘制内容引用的资源 // collectObjectReferences 收集对象和嵌套绘制内容引用的资源
// 入参: object 图形对象, used 引用集合 // 入参: object 图形对象, used 引用集合
func collectObjectReferences(object GraphicObject, used map[string]bool) { func collectObjectReferences(object GraphicObject, used map[string]bool) {
var clips *Clips
switch object.Type { switch object.Type {
case "TextObject": case "TextObject", "Text":
used[object.TextObject.Font], used[object.TextObject.DrawParam] = true, true used[object.TextObject.Font], used[object.TextObject.DrawParam] = true, true
collectPaintReferences(object.TextObject.FillColor, used) collectPaintReferences(object.TextObject.FillColor, used)
collectPaintReferences((*FillColor)(object.TextObject.StrokeColor), used) collectPaintReferences((*FillColor)(object.TextObject.StrokeColor), used)
clips = object.TextObject.Clips
case "PathObject", "Path": case "PathObject", "Path":
used[object.PathObject.DrawParam] = true used[object.PathObject.DrawParam] = true
collectPaintReferences(object.PathObject.FillColor, used) collectPaintReferences(object.PathObject.FillColor, used)
collectPaintReferences((*FillColor)(object.PathObject.StrokeColor), used) collectPaintReferences((*FillColor)(object.PathObject.StrokeColor), used)
clips = object.PathObject.Clips
case "ImageObject": case "ImageObject":
used[object.ImageObject.ResourceID], used[object.ImageObject.ImageMask] = true, true used[object.ImageObject.ResourceID], used[object.ImageObject.ImageMask] = true, true
if object.ImageObject.Border != nil { if object.ImageObject.Border != nil {
collectPaintReferences((*FillColor)(object.ImageObject.Border.BorderColor), used) collectPaintReferences((*FillColor)(object.ImageObject.Border.BorderColor), used)
} }
clips = object.ImageObject.Clips
case "CompositeObject", "CompositeGraphicUnit": case "CompositeObject", "CompositeGraphicUnit":
collectCompositeReferences(object.CompositeGraphicUnit, used) collectCompositeReferences(object.CompositeGraphicUnit, used)
} }
if clips == nil {
return
}
for _, clip := range clips.Clip {
for _, area := range clip.Area {
used[area.DrawParam] = true
for _, path := range area.Path {
collectObjectReferences(GraphicObject{Type: "Path", PathObject: path}, used)
}
for _, text := range area.Text {
collectObjectReferences(GraphicObject{Type: "Text", TextObject: text}, used)
}
}
}
} }
// editorColor 校验当前文档的纯色和渐变 // editorColor 校验当前文档的纯色和渐变
+1 -1
View File
@@ -565,7 +565,7 @@ func (e *Editor) measureCompositeMembers(renderer *Renderer, nodes []*editorComp
capability.ReasonCode, capability.Reason = editReason(err), err.Error() capability.ReasonCode, capability.Reason = editReason(err), err.Error()
errors.As(err, &capability.MissingGlyphs) errors.As(err, &capability.MissingGlyphs)
} else { } else {
capability.TextContent = text.ReadDirection == 0 && text.CharDirection == 0 capability.TextContent = text.ReadDirection == 0 && text.CharDirection == 0 && (text.layout != nil || len(text.CGTransform) == 0)
if _, err := text.TextFrame(); err == nil { if _, err := text.TextFrame(); err == nil {
capability.Reflow = capability.TextContent capability.Reflow = capability.TextContent
capability.LayoutKnown = text.layout != nil capability.LayoutKnown = text.layout != nil
+1
View File
@@ -555,6 +555,7 @@ func (e *Editor) objectCapabilities(object GraphicObject, orderable map[*editorX
} }
if object.Type == "TextObject" { if object.Type == "TextObject" {
all.TextContent = all.Update && object.TextObject.ReadDirection == 0 && object.TextObject.CharDirection == 0 && (all.LayoutKnown || len(object.TextObject.CGTransform) == 0) all.TextContent = all.Update && object.TextObject.ReadDirection == 0 && object.TextObject.CharDirection == 0 && (all.LayoutKnown || len(object.TextObject.CGTransform) == 0)
all.Reflow = all.Reflow && (all.LayoutKnown || len(object.TextObject.CGTransform) == 0)
all.RewriteText = all.TextContent all.RewriteText = all.TextContent
if !all.RewriteText { if !all.RewriteText {
text := object.TextObject text := object.TextObject
+6 -3
View File
@@ -238,8 +238,11 @@ func editorXMLAttributes(node *editorXML, allowed string) bool {
func editorXMLSupported(node *editorXML) bool { func editorXMLSupported(node *editorXML) bool {
var allowed, children string var allowed, children string
switch node.name.Local { switch node.name.Local {
case "TextObject": case "TextObject", "Text":
allowed, children = "ID Boundary CTM DrawParam Font Size HScale Weight ReadDirection CharDirection LineWidth MiterLimit Join Italic Visible Stroke Fill Alpha", "Clips FillColor StrokeColor TextCode" allowed, children = "ID Boundary CTM DrawParam Font Size HScale Weight ReadDirection CharDirection LineWidth MiterLimit Join Italic Visible Stroke Fill Alpha", "Clips FillColor StrokeColor CGTransform TextCode"
case "CGTransform":
allowed, children = "CodePosition CodeCount GlyphCount", "Glyphs"
case "Glyphs":
case "PathObject", "Path": case "PathObject", "Path":
allowed, children = "Boundary CTM LineWidth MiterLimit Join Cap Rule DashPattern DashOffset Visible Stroke Fill Alpha", "StrokeColor FillColor AbbreviatedData" allowed, children = "Boundary CTM LineWidth MiterLimit Join Cap Rule DashPattern DashOffset Visible Stroke Fill Alpha", "StrokeColor FillColor AbbreviatedData"
if node.name.Local == "PathObject" { if node.name.Local == "PathObject" {
@@ -257,7 +260,7 @@ func editorXMLSupported(node *editorXML) bool {
case "Clip": case "Clip":
children = "Area" children = "Area"
case "Area": case "Area":
allowed, children = "CTM", "Path" allowed, children = "CTM", "Path Text"
case "AbbreviatedData": case "AbbreviatedData":
default: default:
return false return false
+163 -4
View File
@@ -39,12 +39,13 @@ func wrapCFFToOTF(cffData []byte) ([]byte, map[rune]uint16, error) {
if err == nil { if err == nil {
cffData = sanitized cffData = sanitized
} }
cffData, err = normalizeCFFDotsection(cffData)
if err != nil {
return nil, nil, err
}
widths, err := parseCFFWidths(cffData, numGlyphs) widths, err := parseCFFWidths(cffData, numGlyphs)
if err != nil { if err != nil {
widths = make([]uint16, numGlyphs) return nil, nil, fmt.Errorf("CFF glyph widths: %w", err)
for i := range widths {
widths[i] = 500
}
} }
var unitsPerEm uint16 = 1000 var unitsPerEm uint16 = 1000
if len(cffData) > 4 { if len(cffData) > 4 {
@@ -82,6 +83,144 @@ func wrapCFFToOTF(cffData []byte) ([]byte, map[rune]uint16, error) {
// cffDict 使用float64存储所有数值,以统一处理整数和实数 // cffDict 使用float64存储所有数值,以统一处理整数和实数
type cffDict map[int][]float64 type cffDict map[int][]float64
// normalizeCFFDotsection 移除旧版Type2字形程序中无效果的dotsection指令
// 入参: data CFF字体数据
// 返回: []byte 标准化后的CFF数据, error 错误信息
func normalizeCFFDotsection(data []byte) ([]byte, error) {
if len(data) < 4 || int(data[2]) >= len(data) {
return nil, fmt.Errorf("invalid CFF header")
}
headerEnd := int(data[2])
_, nameSize := getCFFIndexCount(data, headerEnd)
topStart := headerEnd + nameSize
topData, topSize := getCFFIndexData(data, topStart)
if topData == nil || topStart+topSize > len(data) {
return nil, fmt.Errorf("invalid CFF top dictionary")
}
dict := parseCFFDict(topData)
charOffset := dict[17]
if len(charOffset) != 1 {
return nil, fmt.Errorf("missing CFF charstrings")
}
charStart := int(charOffset[0])
_, charSize := getCFFIndexCount(data, charStart)
if charStart < topStart+topSize || charSize == 0 || charStart+charSize > len(data) {
return nil, fmt.Errorf("invalid CFF charstrings")
}
if !bytes.Contains(data[charStart:charStart+charSize], []byte{12, 0}) {
return data, nil
}
chars := readCFFIndexItems(data, charStart)
changed := false
for index, charstring := range chars {
normalized, removed, err := stripType2Dotsection(charstring)
if err != nil {
return nil, err
}
chars[index] = normalized
changed = changed || removed
}
if !changed {
return data, nil
}
encodedChars := encodeCFFIndex(chars)
topEnd := topStart + topSize
newTop := encodeCFFIndex([][]byte{encodeCFFDict(dict)})
for range 6 {
shift := len(newTop) - topSize
shiftTail := shift + len(encodedChars) - charSize
for _, op := range []int{15, 16, 17, 1236, 1237} {
values := dict[op]
if len(values) != 1 || op == 15 && values[0] <= 2 || op == 16 && values[0] <= 1 {
continue
}
original := parseCFFDict(topData)[op]
offset := int(original[0])
if offset >= charStart+charSize {
dict[op] = []float64{float64(offset + shiftTail)}
} else if offset >= topEnd {
dict[op] = []float64{float64(offset + shift)}
}
}
if values := dict[18]; len(values) == 2 {
original := parseCFFDict(topData)[18]
offset := int(original[1])
if offset >= charStart+charSize {
dict[18] = []float64{values[0], float64(offset + shiftTail)}
}
}
updated := encodeCFFIndex([][]byte{encodeCFFDict(dict)})
if len(updated) == len(newTop) {
newTop = updated
break
}
newTop = updated
}
result := make([]byte, 0, len(data)+len(newTop)-topSize+len(encodedChars)-charSize)
result = append(result, data[:topStart]...)
result = append(result, newTop...)
result = append(result, data[topEnd:charStart]...)
result = append(result, encodedChars...)
result = append(result, data[charStart+charSize:]...)
return result, nil
}
// stripType2Dotsection 保留字形指令与掩码并移除无效果的旧指令
// 入参: data Type2字形程序
// 返回: []byte 标准化后的程序, bool 是否发生变更, error 错误信息
func stripType2Dotsection(data []byte) ([]byte, bool, error) {
var result []byte
stack, hints := 0, 0
for index := 0; index < len(data); {
start := index
operator := int(data[index])
index++
switch {
case operator == 28:
index += 2
stack++
case operator == 255:
index += 4
stack++
case operator >= 247 && operator <= 254:
index++
stack++
case operator >= 32:
stack++
case operator == 12:
if index >= len(data) {
return nil, false, fmt.Errorf("incomplete Type2 operator")
}
operator = 1200 + int(data[index])
index++
if operator == 1200 {
if result == nil {
result = append(make([]byte, 0, len(data)-2), data[:start]...)
}
continue
}
default:
if operator == 1 || operator == 3 || operator == 18 || operator == 23 || operator == 19 || operator == 20 {
hints += stack / 2
}
if operator == 19 || operator == 20 {
index += (hints + 7) / 8
}
stack = 0
}
if index > len(data) {
return nil, false, fmt.Errorf("incomplete Type2 charstring")
}
if result != nil {
result = append(result, data[start:index]...)
}
}
if result == nil {
return data, false, nil
}
return result, true, nil
}
// sanitizeCFF 尝试清洗CFF数据,转换CID字体并合并FontMatrix // sanitizeCFF 尝试清洗CFF数据,转换CID字体并合并FontMatrix
// 入参: data 原始CFF数据 // 入参: data 原始CFF数据
// 返回: []byte 清洗后的CFF数据, error 错误信息 // 返回: []byte 清洗后的CFF数据, error 错误信息
@@ -1501,6 +1640,26 @@ func getUnicodeFromName(name string) rune {
return '.' return '.'
case "slash": case "slash":
return '/' return '/'
case "zero":
return '0'
case "one":
return '1'
case "two":
return '2'
case "three":
return '3'
case "four":
return '4'
case "five":
return '5'
case "six":
return '6'
case "seven":
return '7'
case "eight":
return '8'
case "nine":
return '9'
case "colon": case "colon":
return ':' return ':'
case "semicolon": case "semicolon":
+737
View File
@@ -0,0 +1,737 @@
// 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"
"encoding/hex"
"fmt"
"math"
"strconv"
)
type type1Program struct {
glyphs []type1Glyph
subrs [][]byte
lenIV int
}
type type1Glyph struct {
name string
data []byte
}
type type1Scanner struct {
data []byte
pos int
}
// parseType1Program 读取Type1字体的字形程序及子程序
// 入参: data PFA或PFB字体数据
// 返回: type1Program 字形和子程序, error 错误信息
func parseType1Program(data []byte) (type1Program, error) {
var err error
data, err = flattenType1(data)
if err != nil {
return type1Program{}, err
}
marker := []byte("eexec")
start := bytes.Index(data, marker)
if start < 0 {
return type1Program{}, fmt.Errorf("Type1 font lacks eexec data")
}
start += len(marker)
for start < len(data) && type1Space(data[start]) {
start++
}
if start == len(data) {
return type1Program{}, fmt.Errorf("empty Type1 eexec data")
}
encrypted := data[start:]
if type1HexEncoded(encrypted) {
end := bytes.Index(encrypted, []byte("cleartomark"))
if end >= 0 {
encrypted = encrypted[:end]
}
digits := make([]byte, 0, len(encrypted))
for _, c := range encrypted {
if type1Space(c) {
continue
}
if !type1HexDigit(c) {
break
}
digits = append(digits, c)
}
encrypted = make([]byte, hex.DecodedLen(len(digits)))
if _, err := hex.Decode(encrypted, digits); err != nil {
return type1Program{}, fmt.Errorf("invalid Type1 eexec encoding: %w", err)
}
}
if len(encrypted) < 4 {
return type1Program{}, fmt.Errorf("invalid Type1 eexec data")
}
decrypted := type1Decrypt(encrypted, 55665)[4:]
scanner := type1Scanner{data: decrypted}
program := type1Program{lenIV: 4}
section := ""
for {
token := scanner.next()
if token == "" || token == "closefile" {
break
}
switch token {
case "/lenIV":
value, err := strconv.Atoi(scanner.next())
if err != nil || value < -1 {
return type1Program{}, fmt.Errorf("invalid Type1 lenIV")
}
program.lenIV = value
case "/Subrs":
section = "subrs"
case "/CharStrings":
section = "glyphs"
case "dup":
if section != "subrs" {
continue
}
index, err := strconv.Atoi(scanner.next())
if err != nil || index < 0 || index > len(decrypted) {
return type1Program{}, fmt.Errorf("invalid Type1 subroutine index")
}
length, err := strconv.Atoi(scanner.next())
if err != nil {
return type1Program{}, fmt.Errorf("invalid Type1 subroutine length")
}
if !type1BinaryStart(scanner.next()) {
return type1Program{}, fmt.Errorf("missing Type1 subroutine data")
}
raw, err := scanner.binary(length)
if err != nil {
return type1Program{}, err
}
for len(program.subrs) <= index {
program.subrs = append(program.subrs, nil)
}
program.subrs[index] = raw
default:
if section != "glyphs" || len(token) < 2 || token[0] != '/' {
continue
}
name := token[1:]
next := scanner.next()
length, err := strconv.Atoi(next)
if err != nil {
continue
}
if !type1BinaryStart(scanner.next()) {
return type1Program{}, fmt.Errorf("missing Type1 glyph data")
}
raw, err := scanner.binary(length)
if err != nil {
return type1Program{}, err
}
program.glyphs = append(program.glyphs, type1Glyph{name: name, data: raw})
}
}
if len(program.glyphs) == 0 {
return type1Program{}, fmt.Errorf("Type1 font has no glyphs")
}
for index := range program.subrs {
program.subrs[index], err = type1CharString(program.subrs[index], program.lenIV)
if err != nil {
return type1Program{}, err
}
}
for index := range program.glyphs {
program.glyphs[index].data, err = type1CharString(program.glyphs[index].data, program.lenIV)
if err != nil {
return type1Program{}, fmt.Errorf("Type1 glyph %s: %w", program.glyphs[index].name, err)
}
}
return program, nil
}
// flattenType1 将PFB分段容器转换为连续的Type1程序
// 入参: data 字体数据
// 返回: []byte 连续程序, error 错误信息
func flattenType1(data []byte) ([]byte, error) {
if len(data) < 2 || data[0] != 0x80 {
return data, nil
}
var program []byte
for len(data) >= 2 && data[0] == 0x80 {
if data[1] == 3 {
return program, nil
}
if len(data) < 6 || data[1] != 1 && data[1] != 2 {
return nil, fmt.Errorf("invalid PFB segment")
}
length := binary.LittleEndian.Uint32(data[2:6])
if uint64(length) > uint64(len(data)-6) {
return nil, fmt.Errorf("truncated PFB segment")
}
program = append(program, data[6:6+length]...)
data = data[6+length:]
}
return nil, fmt.Errorf("missing PFB end marker")
}
// type1Decrypt 解密Type1的eexec或字形程序
// 入参: data 密文, seed 初始密钥
// 返回: []byte 明文
func type1Decrypt(data []byte, seed uint16) []byte {
out := make([]byte, len(data))
for index, value := range data {
out[index] = value ^ byte(seed>>8)
seed = uint16((uint32(value)+uint32(seed))*52845 + 22719)
}
return out
}
// type1CharString 解密字形程序并移除lenIV随机前缀
// 入参: data 字形密文, lenIV 前缀长度
// 返回: []byte 指令数据, error 错误信息
func type1CharString(data []byte, lenIV int) ([]byte, error) {
if lenIV < 0 {
return data, nil
}
if len(data) < lenIV {
return nil, fmt.Errorf("truncated Type1 charstring")
}
return type1Decrypt(data, 4330)[lenIV:], nil
}
// type1HexEncoded 区分eexec十六进制与二进制封装
// 入参: data eexec数据
// 返回: bool 是否为十六进制
func type1HexEncoded(data []byte) bool {
count := 0
for _, value := range data {
if type1Space(value) {
continue
}
if !type1HexDigit(value) {
return false
}
count++
if count == 8 {
return true
}
}
return false
}
func type1HexDigit(value byte) bool {
return value >= '0' && value <= '9' || value >= 'a' && value <= 'f' || value >= 'A' && value <= 'F'
}
func type1Space(value byte) bool {
return value == ' ' || value == '\t' || value == '\r' || value == '\n' || value == '\f' || value == 0
}
// next 读取下一个PostScript词项,字形二进制块由binary处理
// 返回: string 词项
func (s *type1Scanner) next() string {
for s.pos < len(s.data) {
if type1Space(s.data[s.pos]) {
s.pos++
continue
}
if s.data[s.pos] == '%' {
for s.pos < len(s.data) && s.data[s.pos] != '\n' && s.data[s.pos] != '\r' {
s.pos++
}
continue
}
break
}
if s.pos == len(s.data) {
return ""
}
start := s.pos
if bytes.IndexByte([]byte("[]{}"), s.data[s.pos]) >= 0 {
s.pos++
return string(s.data[start:s.pos])
}
for s.pos < len(s.data) && !type1Space(s.data[s.pos]) && bytes.IndexByte([]byte("[]{}%"), s.data[s.pos]) < 0 {
s.pos++
}
return string(s.data[start:s.pos])
}
// binary 按PostScript长度读取RD后的加密字节
// 入参: length 数据长度
// 返回: []byte 二进制数据, error 错误信息
func (s *type1Scanner) binary(length int) ([]byte, error) {
if s.pos >= len(s.data) || !type1Space(s.data[s.pos]) || length < 0 {
return nil, fmt.Errorf("invalid Type1 binary data")
}
if s.data[s.pos] == '\r' && s.pos+1 < len(s.data) && s.data[s.pos+1] == '\n' {
s.pos += 2
} else {
s.pos++
}
if length > len(s.data)-s.pos {
return nil, fmt.Errorf("truncated Type1 binary data")
}
data := s.data[s.pos : s.pos+length]
s.pos += length
return data, nil
}
func type1BinaryStart(token string) bool {
return token == "RD" || token == "-|"
}
type type1Outline struct {
program *type1Program
args []float64
postscript []float64
flexPoints [][2]float64
width float64
x float64
y float64
pointX float64
pointY float64
open bool
defined bool
flex bool
path bytes.Buffer
}
// outline 将Type1字形指令转换为保留三次曲线的Type2程序
// 入参: glyph 字形数据
// 返回: []byte Type2字形程序, error 错误信息
func (p *type1Program) outline(glyph type1Glyph) ([]byte, error) {
state := type1Outline{program: p}
ended, err := state.run(glyph.data, 0)
if err != nil {
return nil, fmt.Errorf("Type1 glyph %s: %w", glyph.name, err)
}
if !ended || !state.defined {
return nil, fmt.Errorf("Type1 glyph %s has no complete outline", glyph.name)
}
var result bytes.Buffer
if err := encodeType2Number(&result, state.width); err != nil {
return nil, err
}
result.Write(state.path.Bytes())
result.WriteByte(14)
return result.Bytes(), nil
}
// run 执行Type1字形或子程序,遇到endchar返回完成状态
// 入参: data 指令数据, depth 子程序深度
// 返回: bool 是否遇到endchar, error 错误信息
func (s *type1Outline) run(data []byte, depth int) (bool, error) {
if depth > 32 {
return false, fmt.Errorf("Type1 subroutine recursion")
}
for index := 0; index < len(data); index++ {
op := int(data[index])
if op >= 32 {
var value float64
switch {
case op <= 246:
value = float64(op - 139)
case op <= 250:
if index+1 >= len(data) {
return false, fmt.Errorf("truncated Type1 operand")
}
index++
value = float64((op-247)*256 + int(data[index]) + 108)
case op <= 254:
if index+1 >= len(data) {
return false, fmt.Errorf("truncated Type1 operand")
}
index++
value = float64(-(op-251)*256 - int(data[index]) - 108)
default:
if index+4 >= len(data) {
return false, fmt.Errorf("truncated Type1 operand")
}
value = float64(int32(binary.BigEndian.Uint32(data[index+1 : index+5])))
index += 4
}
s.args = append(s.args, value)
continue
}
if op == 12 {
if index+1 >= len(data) {
return false, fmt.Errorf("truncated Type1 operator")
}
index++
op = 1200 + int(data[index])
}
if op == 10 {
if len(s.args) == 0 {
return false, fmt.Errorf("Type1 callsubr has no index")
}
index := int(s.args[len(s.args)-1])
s.args = s.args[:len(s.args)-1]
if index < 0 || index >= len(s.program.subrs) || s.program.subrs[index] == nil {
return false, fmt.Errorf("invalid Type1 subroutine index %d", index)
}
ended, err := s.run(s.program.subrs[index], depth+1)
if err != nil || ended {
return ended, err
}
continue
}
if op == 11 {
if depth == 0 {
return false, fmt.Errorf("Type1 return outside subroutine")
}
return false, nil
}
if op == 14 {
return true, nil
}
if err := s.operator(op); err != nil {
return false, err
}
}
return false, nil
}
// operator 执行单个Type1绘制指令并清空已消费的操作数
// 入参: op 指令编号
// 返回: error 错误信息
func (s *type1Outline) operator(op int) error {
a := s.args
s.args = nil
switch op {
case 1, 3, 1200, 1201, 1202:
return nil
case 13:
if s.defined || len(a) != 2 {
return fmt.Errorf("invalid Type1 hsbw")
}
s.x, s.width, s.defined = a[0], a[1], true
case 1207:
if s.defined || len(a) != 4 || a[3] != 0 {
return fmt.Errorf("invalid Type1 sbw")
}
s.x, s.y, s.width, s.defined = a[0], a[1], a[2], true
case 4, 21, 22:
if !s.defined || op == 4 && len(a) != 1 || op == 21 && len(a) != 2 || op == 22 && len(a) != 1 {
return fmt.Errorf("invalid Type1 move")
}
if s.flex {
point := [2]float64{}
if op == 4 || op == 21 {
point[1] = a[len(a)-1]
}
if op == 21 || op == 22 {
point[0] = a[0]
}
s.flexPoints = append(s.flexPoints, point)
return nil
}
if op == 4 || op == 21 {
s.y += a[len(a)-1]
}
if op == 21 || op == 22 {
s.x += a[0]
}
return s.move()
case 5, 6, 7:
if !s.defined || op == 5 && (len(a) == 0 || len(a)%2 != 0) || op != 5 && len(a) == 0 {
return fmt.Errorf("invalid Type1 line")
}
if err := s.ensureMove(); err != nil {
return err
}
for n := 0; n < len(a); {
var dx, dy float64
switch op {
case 5:
dx, dy = a[n], a[n+1]
n += 2
case 6:
dx = a[n]
n++
case 7:
dy = a[n]
n++
}
if err := s.line(dx, dy); err != nil {
return err
}
}
case 8, 30, 31:
count := 6
if op != 8 {
count = 4
}
if !s.defined || len(a) == 0 || len(a)%count != 0 {
return fmt.Errorf("invalid Type1 curve")
}
if err := s.ensureMove(); err != nil {
return err
}
for n := 0; n < len(a); n += count {
var curve [6]float64
switch op {
case 8:
copy(curve[:], a[n:n+6])
case 30:
curve = [6]float64{0, a[n], a[n+1], a[n+2], a[n+3], 0}
case 31:
curve = [6]float64{a[n], 0, a[n+1], a[n+2], 0, a[n+3]}
}
if err := s.curve(curve); err != nil {
return err
}
}
case 9:
s.open = false
case 1212:
if len(a) != 2 || a[1] == 0 {
return fmt.Errorf("invalid Type1 division")
}
s.args = []float64{a[0] / a[1]}
case 1216:
if len(a) < 2 {
return fmt.Errorf("invalid Type1 callothersubr")
}
count, number := int(a[len(a)-2]), int(a[len(a)-1])
if count < 0 || count != len(a)-2 {
return fmt.Errorf("invalid Type1 callothersubr arguments")
}
switch number {
case 0:
if !s.flex || count != 3 || len(s.flexPoints) != 7 {
return fmt.Errorf("invalid Type1 Flex end")
}
if err := s.ensureMove(); err != nil {
return err
}
first := s.flexPoints
if err := s.curve([6]float64{first[0][0] + first[1][0], first[0][1] + first[1][1], first[2][0], first[2][1], first[3][0], first[3][1]}); err != nil {
return err
}
if err := s.curve([6]float64{first[4][0], first[4][1], first[5][0], first[5][1], first[6][0], first[6][1]}); err != nil {
return err
}
if s.x != a[1] || s.y != a[2] {
return fmt.Errorf("Type1 Flex endpoint differs from outline")
}
s.postscript = []float64{s.x, s.y}
s.flexPoints = nil
s.flex = false
case 1:
if count != 0 || s.flex {
return fmt.Errorf("invalid Type1 Flex start")
}
s.flex = true
s.flexPoints = nil
case 2:
if count != 0 || !s.flex {
return fmt.Errorf("invalid Type1 Flex point")
}
case 3:
if count != 1 {
return fmt.Errorf("invalid Type1 hint replacement")
}
s.postscript = []float64{a[0]}
default:
return fmt.Errorf("unsupported Type1 OtherSubr %d", number)
}
case 1217:
if len(s.postscript) == 0 {
return fmt.Errorf("invalid Type1 pop")
}
s.args = append(a, s.postscript[0])
s.postscript = s.postscript[1:]
case 1233:
if len(a) != 2 {
return fmt.Errorf("invalid Type1 setcurrentpoint")
}
s.x, s.y = a[0], a[1]
case 1206:
return fmt.Errorf("unsupported Type1 seac")
default:
return fmt.Errorf("unsupported Type1 operator %d", op)
}
return nil
}
// move 在Type2字形程序中建立新轮廓
// 返回: error 错误信息
func (s *type1Outline) move() error {
if err := encodeType2Number(&s.path, s.x-s.pointX); err != nil {
return err
}
if err := encodeType2Number(&s.path, s.y-s.pointY); err != nil {
return err
}
s.path.WriteByte(21)
s.pointX, s.pointY, s.open = s.x, s.y, true
return nil
}
// ensureMove 在首次描绘或关闭轮廓后建立当前位置
// 返回: error 错误信息
func (s *type1Outline) ensureMove() error {
if !s.open || s.x != s.pointX || s.y != s.pointY {
return s.move()
}
return nil
}
// line 保留Type1线段的实际终点
// 入参: dx 横向增量, dy 纵向增量
// 返回: error 错误信息
func (s *type1Outline) line(dx, dy float64) error {
if err := encodeType2Number(&s.path, dx); err != nil {
return err
}
if err := encodeType2Number(&s.path, dy); err != nil {
return err
}
s.path.WriteByte(5)
s.x += dx
s.y += dy
s.pointX, s.pointY = s.x, s.y
return nil
}
// curve 保留Type1三次贝塞尔控制点
// 入参: values 六个相对坐标
// 返回: error 错误信息
func (s *type1Outline) curve(values [6]float64) error {
for _, value := range values {
if err := encodeType2Number(&s.path, value); err != nil {
return err
}
}
s.path.WriteByte(8)
s.x += values[0] + values[2] + values[4]
s.y += values[1] + values[3] + values[5]
s.pointX, s.pointY = s.x, s.y
return nil
}
// encodeType2Number 编码Type2字形指令的有符号定点操作数
// 入参: target 目标字节流, value 操作数
// 返回: error 错误信息
func encodeType2Number(target *bytes.Buffer, value float64) error {
if !finite(value) || value < -32768 || value >= 32768 {
return fmt.Errorf("Type2 operand out of range")
}
if value == math.Trunc(value) {
integer := int(value)
switch {
case integer >= -107 && integer <= 107:
target.WriteByte(byte(integer + 139))
case integer >= 108 && integer <= 1131:
integer -= 108
target.WriteByte(byte(247 + integer/256))
target.WriteByte(byte(integer % 256))
case integer <= -108 && integer >= -1131:
integer = -integer - 108
target.WriteByte(byte(251 + integer/256))
target.WriteByte(byte(integer % 256))
default:
target.WriteByte(28)
_ = binary.Write(target, binary.BigEndian, int16(integer))
}
return nil
}
target.WriteByte(255)
_ = binary.Write(target, binary.BigEndian, int32(math.Round(value*65536)))
return nil
}
// toCFF 将Type1字形转换为保持三次轮廓的CFF字体
// 入参: name 字体名称
// 返回: []byte CFF数据, error 错误信息
func (p *type1Program) toCFF(name string) ([]byte, error) {
if len(p.glyphs) > math.MaxUint16 || name == "" {
return nil, fmt.Errorf("invalid Type1 font identity")
}
ordered := make([]type1Glyph, 0, len(p.glyphs))
for _, glyph := range p.glyphs {
if glyph.name == ".notdef" {
ordered = append(ordered, glyph)
break
}
}
if len(ordered) == 0 {
return nil, fmt.Errorf("Type1 font lacks .notdef glyph")
}
for _, glyph := range p.glyphs {
if glyph.name != ".notdef" {
ordered = append(ordered, glyph)
}
}
chars := make([][]byte, len(ordered))
strings := make([][]byte, 0, len(ordered)-1)
charset := make([]byte, 1+2*(len(ordered)-1))
for index, glyph := range ordered {
charstring, err := p.outline(glyph)
if err != nil {
return nil, err
}
chars[index] = charstring
if index == 0 {
continue
}
strings = append(strings, []byte(glyph.name))
sid := 391 + index - 1
if sid > math.MaxUint16 {
return nil, fmt.Errorf("Type1 font has too many glyph names")
}
binary.BigEndian.PutUint16(charset[1+(index-1)*2:], uint16(sid))
}
nameIndex := encodeCFFIndex([][]byte{[]byte(name)})
stringIndex := encodeCFFIndex(strings)
globalSubrs := encodeCFFIndex(nil)
charIndex := encodeCFFIndex(chars)
topDict := make([]byte, 12)
topDict[0], topDict[5], topDict[6], topDict[11] = 29, 15, 29, 17
topIndex := encodeCFFIndex([][]byte{topDict})
charsetOffset := 4 + len(nameIndex) + len(topIndex) + len(stringIndex) + len(globalSubrs)
charOffset := charsetOffset + len(charset)
binary.BigEndian.PutUint32(topDict[1:5], uint32(charsetOffset))
binary.BigEndian.PutUint32(topDict[7:11], uint32(charOffset))
topIndex = encodeCFFIndex([][]byte{topDict})
data := make([]byte, 0, charOffset+len(charIndex))
data = append(data, 1, 0, 4, 4)
data = append(data, nameIndex...)
data = append(data, topIndex...)
data = append(data, stringIndex...)
data = append(data, globalSubrs...)
data = append(data, charset...)
data = append(data, charIndex...)
return data, nil
}
// glyphIDs 返回Type1名称到转换后字形编号的对应关系
// 返回: map[string]uint16 字形编号
func (p *type1Program) glyphIDs() map[string]uint16 {
ids := map[string]uint16{".notdef": 0}
next := uint16(1)
for _, glyph := range p.glyphs {
if glyph.name != ".notdef" {
ids[glyph.name] = next
next++
}
}
return ids
}
+52 -28
View File
@@ -48,24 +48,27 @@ type PDFImportReport struct {
// pdfImporter 保存单次转换的对象与资源状态 // pdfImporter 保存单次转换的对象与资源状态
type pdfImporter struct { type pdfImporter struct {
ctx context.Context ctx context.Context
reader *pdfgo.Reader reader *pdfgo.Reader
editor *Editor editor *Editor
report PDFImportReport report PDFImportReport
matrix pdfgo.Matrix matrix pdfgo.Matrix
page int page int
fontIDs map[*pdfgo.Font]string fontIDs map[*pdfgo.Font]string
fontMetrics map[string]FontMetrics fontMetrics map[string]FontMetrics
cmykSpace string fontRepairLimit map[*pdfgo.Font]uint16
objects []GraphicObject type1Glyphs map[*pdfgo.Font]map[string]uint16
pages map[pdfgo.Reference]*pdfgo.Page clipTexts map[*pdfgo.TextClip]TextObject
pageIDs map[pdfgo.Reference]string cmykSpace string
imageIDs map[*pdfgo.Stream]string objects []GraphicObject
maskClips map[*pdfgo.SoftMask]pdfgo.Path pages map[pdfgo.Reference]*pdfgo.Page
pageBox pdfgo.Rectangle pageIDs map[pdfgo.Reference]string
pageWidth float64 imageIDs map[*pdfgo.Stream]string
pageHeight float64 maskClips map[*pdfgo.SoftMask]pdfgo.Path
pendingPath *pdfgo.PathMark pageBox pdfgo.Rectangle
pageWidth float64
pageHeight float64
pendingPath *pdfgo.PathMark
} }
// ImportPDF 将PDF内容转换为独立OFD编辑文档,失败时不返回部分结果 // ImportPDF 将PDF内容转换为独立OFD编辑文档,失败时不返回部分结果
@@ -122,6 +125,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
importer.pageWidth = width importer.pageWidth = width
importer.pageHeight = height importer.pageHeight = height
importer.maskClips = make(map[*pdfgo.SoftMask]pdfgo.Path) importer.maskClips = make(map[*pdfgo.SoftMask]pdfgo.Path)
importer.clipTexts = make(map[*pdfgo.TextClip]TextObject)
visitor := pdfgo.Visitor{Path: importer.path, Text: importer.text, Image: importer.image} visitor := pdfgo.Visitor{Path: importer.path, Text: importer.text, Image: importer.image}
visitor.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error { visitor.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
return importer.group(mark, walk, visitor) return importer.group(mark, walk, visitor)
@@ -293,28 +297,44 @@ func (p *pdfImporter) pathData(path pdfgo.Path, origin Box) string {
// clips 保持裁剪路径的交集,不随对象CTM重复变换 // clips 保持裁剪路径的交集,不随对象CTM重复变换
// 入参: paths 裁剪路径, origin 对象边界 // 入参: paths 裁剪路径, origin 对象边界
// 返回: *Clips OFD裁剪区域,无裁剪时为空 // 返回: *Clips OFD裁剪区域,无裁剪时为空, error 未解析的裁剪字形
func (p *pdfImporter) clips(paths []pdfgo.Path, origin Box) *Clips { func (p *pdfImporter) clips(paths []pdfgo.Path, origin Box) (*Clips, error) {
if len(paths) == 0 { if len(paths) == 0 {
return nil return nil, nil
} }
flag := false flag := false
clips := &Clips{TransFlag: &flag} clips := &Clips{TransFlag: &flag}
for _, path := range paths { for _, path := range paths {
area := ClipArea{}
var points []pdfgo.Point var points []pdfgo.Point
for _, segment := range path.Segments { for _, segment := range path.Segments {
for _, point := range segment.Points { for _, point := range segment.Points {
points = append(points, p.matrix.Apply(point)) points = append(points, p.matrix.Apply(point))
} }
} }
box := pdfBounds(points) if len(path.Segments) != 0 {
rule := "NonZero" box := pdfBounds(points)
if path.EvenOdd { rule := "NonZero"
rule = "Even-Odd" if path.EvenOdd {
rule = "Even-Odd"
}
area.Path = append(area.Path, PathObject{Boundary: pdfBoundary(Box{box.X - origin.X, box.Y - origin.Y, box.W, box.H}), AbbreviatedData: p.pathData(path, box), Rule: rule})
} }
clips.Clip = append(clips.Clip, Clip{Area: []ClipArea{{Path: []PathObject{{Boundary: pdfBoundary(Box{box.X - origin.X, box.Y - origin.Y, box.W, box.H}), AbbreviatedData: p.pathData(path, box), Rule: rule}}}}}) for _, mark := range path.Text {
object, ok := p.clipTexts[mark]
if !ok {
return nil, fmt.Errorf("missing PDF text clipping glyphs")
}
box, err := ParseBox(object.Boundary)
if err != nil {
return nil, err
}
object.Boundary = pdfBoundary(Box{box.X - origin.X, box.Y - origin.Y, box.W, box.H})
area.Text = append(area.Text, object)
}
clips.Clip = append(clips.Clip, Clip{Area: []ClipArea{area}})
} }
return clips return clips, nil
} }
// path 合并相同状态下的不透明描边,保留独立子路径与绘制顺序 // path 合并相同状态下的不透明描边,保留独立子路径与绘制顺序
@@ -393,7 +413,11 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
return err return err
} }
strokeColor := StrokeColor(*strokePaint) strokeColor := StrokeColor(*strokePaint)
object := PathObject{Boundary: pdfBoundary(box), AbbreviatedData: p.pathData(mark.Path, box), Fill: &mark.Fill, Stroke: &mark.Stroke, FillColor: fillColor, StrokeColor: &strokeColor, LineWidth: mark.Style.LineWidth * scale, Cap: []string{"Butt", "Round", "Square"}[mark.Style.Cap], Join: []string{"Miter", "Round", "Bevel"}[mark.Style.Join], MiterLimit: mark.Style.MiterLimit, Clips: p.clips(mark.Style.Clips, box)} clips, err := p.clips(mark.Style.Clips, box)
if err != nil {
return err
}
object := PathObject{Boundary: pdfBoundary(box), AbbreviatedData: p.pathData(mark.Path, box), Fill: &mark.Fill, Stroke: &mark.Stroke, FillColor: fillColor, StrokeColor: &strokeColor, LineWidth: mark.Style.LineWidth * scale, Cap: []string{"Butt", "Round", "Square"}[mark.Style.Cap], Join: []string{"Miter", "Round", "Bevel"}[mark.Style.Join], MiterLimit: mark.Style.MiterLimit, Clips: clips}
object.LineWidthSet = object.LineWidth == 0 object.LineWidthSet = object.LineWidth == 0
if mark.Path.EvenOdd { if mark.Path.EvenOdd {
object.Rule = "Even-Odd" object.Rule = "Even-Odd"
+5 -1
View File
@@ -148,7 +148,11 @@ func (p *pdfImporter) group(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error
} }
region.paint.Alpha = mark.Alpha region.paint.Alpha = mark.Alpha
fill, stroke := true, false fill, stroke := true, false
object := PathObject{Boundary: pdfBoundary(box), AbbreviatedData: data, Fill: &fill, Stroke: &stroke, FillColor: p.color(region.paint), Clips: p.clips(clips, box)} objectClips, err := p.clips(clips, box)
if err != nil {
return err
}
object := PathObject{Boundary: pdfBoundary(box), AbbreviatedData: data, Fill: &fill, Stroke: &stroke, FillColor: p.color(region.paint), Clips: objectClips}
p.objects = append(p.objects, GraphicObject{Type: "PathObject", PathObject: object}) p.objects = append(p.objects, GraphicObject{Type: "PathObject", PathObject: object})
p.report.PathObjects++ p.report.PathObjects++
} }
+181 -29
View File
@@ -75,20 +75,28 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
} }
if source.ImageMask { if source.ImageMask {
fill := mark.Style.Fill.RGB fill := mark.Style.Fill.RGB
tint := color.NRGBA64{
R: uint16(math.Round(fill[0] * 65535)),
G: uint16(math.Round(fill[1] * 65535)),
B: uint16(math.Round(fill[2] * 65535)),
}
if mark.Style.Fill.CMYK != nil { if mark.Style.Fill.CMYK != nil {
if *mark.Style.Fill.CMYK != [4]float64{} { components := *mark.Style.Fill.CMYK
return &pdfgo.UnsupportedError{Feature: "CMYK stencil image"} tint = color.NRGBA64Model.Convert(color.CMYK{
} C: uint8(math.Round(components[0] * 255)),
fill = [3]float64{1, 1, 1} M: uint8(math.Round(components[1] * 255)),
Y: uint8(math.Round(components[2] * 255)),
K: uint8(math.Round(components[3] * 255)),
}).(color.NRGBA64)
} }
stencil := image.NewNRGBA64(image.Rect(0, 0, source.Width, source.Height)) stencil := image.NewNRGBA64(image.Rect(0, 0, source.Width, source.Height))
for y := 0; y < source.Height; y++ { for y := 0; y < source.Height; y++ {
for x := 0; x < source.Width; x++ { for x := 0; x < source.Width; x++ {
gray, _, _, _ := decoded.At(x, y).RGBA() gray, _, _, _ := decoded.At(x, y).RGBA()
stencil.SetNRGBA64(x, y, color.NRGBA64{ stencil.SetNRGBA64(x, y, color.NRGBA64{
R: uint16(math.Round(fill[0] * 65535)), R: tint.R,
G: uint16(math.Round(fill[1] * 65535)), G: tint.G,
B: uint16(math.Round(fill[2] * 65535)), B: tint.B,
A: uint16(65535 - gray), A: uint16(65535 - gray),
}) })
} }
@@ -119,7 +127,11 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
m := p.matrix.Mul(mark.Matrix).Mul(pdfgo.Matrix{1, 0, 0, -1, 0, 1}) m := p.matrix.Mul(mark.Matrix).Mul(pdfgo.Matrix{1, 0, 0, -1, 0, 1})
box := pdfBounds([]pdfgo.Point{m.Apply(pdfgo.Point{}), m.Apply(pdfgo.Point{X: 1}), m.Apply(pdfgo.Point{Y: 1}), m.Apply(pdfgo.Point{X: 1, Y: 1})}) box := pdfBounds([]pdfgo.Point{m.Apply(pdfgo.Point{}), m.Apply(pdfgo.Point{X: 1}), m.Apply(pdfgo.Point{Y: 1}), m.Apply(pdfgo.Point{X: 1, Y: 1})})
alpha := int(math.Round(mark.Style.Fill.Alpha * 255)) alpha := int(math.Round(mark.Style.Fill.Alpha * 255))
object := ImageObject{Boundary: pdfBoundary(box), ResourceID: id, CTM: pdfNumbers(m[0], m[1], m[2], m[3], m[4]-box.X, m[5]-box.Y), Alpha: &alpha, Clips: p.clips(mark.Style.Clips, box)} clips, err := p.clips(mark.Style.Clips, box)
if err != nil {
return err
}
object := ImageObject{Boundary: pdfBoundary(box), ResourceID: id, CTM: pdfNumbers(m[0], m[1], m[2], m[3], m[4]-box.X, m[5]-box.Y), Alpha: &alpha, Clips: clips}
p.objects = append(p.objects, GraphicObject{Type: "ImageObject", ImageObject: object}) p.objects = append(p.objects, GraphicObject{Type: "ImageObject", ImageObject: object})
p.report.ImageObjects++ p.report.ImageObjects++
return nil return nil
@@ -130,6 +142,9 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
// 返回: error 错误信息 // 返回: error 错误信息
func (p *pdfImporter) text(mark pdfgo.TextMark) error { func (p *pdfImporter) text(mark pdfgo.TextMark) error {
if mark.Font.Subtype == pdfgo.Name("Type3") { if mark.Font.Subtype == pdfgo.Name("Type3") {
if mark.Clip != nil {
return &pdfgo.UnsupportedError{Feature: "Type3 text clipping"}
}
if err := p.flushPath(); err != nil { if err := p.flushPath(); err != nil {
return err return err
} }
@@ -155,23 +170,44 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
if err != nil { if err != nil {
return err return err
} }
if (mark.Mode == 0 || mark.Mode == 2) && mark.Style.FillOverprint && pdfOverprintNeedsSeparation(mark.Style.Fill) || (mark.Mode == 1 || mark.Mode == 2) && mark.Style.StrokeOverprint && pdfOverprintNeedsSeparation(mark.Style.Stroke) { paintMode := mark.Mode % 4
if (paintMode == 0 || paintMode == 2) && mark.Style.FillOverprint && pdfOverprintNeedsSeparation(mark.Style.Fill) || (paintMode == 1 || paintMode == 2) && mark.Style.StrokeOverprint && pdfOverprintNeedsSeparation(mark.Style.Stroke) {
return &pdfgo.UnsupportedError{Feature: "text color separation overprint"} return &pdfgo.UnsupportedError{Feature: "text color separation overprint"}
} }
font := mark.Font font := mark.Font
embedded := len(font.Program) != 0 embedded := len(font.Program) != 0
if embedded && font.ProgramType != "FontFile2" && font.ProgramType != "OpenType" && font.ProgramType != "Type1C" && font.ProgramType != "CIDFontType0C" { if embedded && font.ProgramType != "FontFile" && font.ProgramType != "FontFile2" && font.ProgramType != "OpenType" && font.ProgramType != "Type1C" && font.ProgramType != "CIDFontType0C" {
return &pdfgo.UnsupportedError{Feature: "font program " + string(font.ProgramType)} return &pdfgo.UnsupportedError{Feature: "font program " + string(font.ProgramType)}
} }
id := p.fontIDs[font] id := p.fontIDs[font]
if id == "" { if id == "" {
var err error var err error
if embedded { if embedded {
var type1 *type1Program
if font.ProgramType == "FontFile" {
parsed, err := parseType1Program(font.Program)
if err != nil {
return fmt.Errorf("PDF font %s program: %w", font.Name, err)
}
type1 = &parsed
if p.type1Glyphs == nil {
p.type1Glyphs = make(map[*pdfgo.Font]map[string]uint16)
}
p.type1Glyphs[font] = parsed.glyphIDs()
}
var program []byte var program []byte
program, err = pdfFontProgram(font) var limit uint16
program, limit, err = pdfFontProgram(font, type1)
if err != nil { if err != nil {
return fmt.Errorf("PDF font %s program: %w", font.Name, err) return fmt.Errorf("PDF font %s program: %w", font.Name, err)
} }
if limit != 0 {
if p.fontRepairLimit == nil {
p.fontRepairLimit = make(map[*pdfgo.Font]uint16)
}
p.fontRepairLimit[font] = limit
p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF embedded font lacks trailing side bearings; unused metrics reconstructed from glyph bounds"})
}
id, err = p.editor.AddFont(FontFile{Name: font.Name + ".ttf", Data: program}, 0) id, err = p.editor.AddFont(FontFile{Name: font.Name + ".ttf", Data: program}, 0)
} else { } else {
id, err = p.editor.AddExternalFont(font.Name) id, err = p.editor.AddExternalFont(font.Name)
@@ -181,6 +217,13 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
} }
p.fontIDs[font] = id p.fontIDs[font] = id
} }
if limit := p.fontRepairLimit[font]; limit != 0 {
for _, glyph := range mark.Glyphs {
if glyph.HasID && glyph.ID >= limit {
return &pdfgo.UnsupportedError{Feature: "embedded font glyph with missing side bearing"}
}
}
}
var metrics FontMetrics var metrics FontMetrics
var outline FontOutlines var outline FontOutlines
if embedded { if embedded {
@@ -208,6 +251,14 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
position := 0 position := 0
for n, glyph := range mark.Glyphs { for n, glyph := range mark.Glyphs {
gid := glyph.ID gid := glyph.ID
if embedded && font.ProgramType == "FontFile" {
var found bool
gid, found = p.type1Glyphs[font][glyph.Name]
if !found {
return fmt.Errorf("missing Type1 glyph %q in PDF font %s", glyph.Name, font.Name)
}
glyph.HasID = true
}
if glyph.Text == "" && glyph.HasID { if glyph.Text == "" && glyph.HasID {
character := getUnicodeFromName(glyph.Name) character := getUnicodeFromName(glyph.Name)
if character == 0 { if character == 0 {
@@ -256,7 +307,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
} }
} }
box := pdfBounds(points) box := pdfBounds(points)
fill, stroke, visible := mark.Mode == 0 || mark.Mode == 2, mark.Mode == 1 || mark.Mode == 2, mark.Mode != 3 fill, stroke, visible := paintMode == 0 || paintMode == 2, paintMode == 1 || paintMode == 2, paintMode != 3
if stroke { if stroke {
scale := math.Sqrt(math.Abs(m[0]*m[3] - m[1]*m[2])) scale := math.Sqrt(math.Abs(m[0]*m[3] - m[1]*m[2]))
if scale == 0 || math.IsNaN(scale) || math.IsInf(scale, 0) { if scale == 0 || math.IsNaN(scale) || math.IsInf(scale, 0) {
@@ -292,44 +343,81 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
if err != nil { if err != nil {
return err return err
} }
object.Clips = p.clips(mark.Style.Clips, box) object.Clips, err = p.clips(mark.Style.Clips, box)
if err != nil {
return err
}
if mark.Clip != nil {
clipObject := object
clipObject.Clips = nil
clipObject.Fill, clipObject.Stroke = new(bool), new(bool)
*clipObject.Fill = true
*clipObject.Stroke = false
clipObject.Visible = nil
p.clipTexts[mark.Clip] = clipObject
}
p.objects = append(p.objects, GraphicObject{Type: "TextObject", TextObject: object}) p.objects = append(p.objects, GraphicObject{Type: "TextObject", TextObject: object})
p.report.TextObjects++ p.report.TextObjects++
return nil return nil
} }
// pdfFontProgram 为PDF子集字体补齐封装表,保留字形轮廓和编号 // pdfFontProgram 为PDF子集字体补齐封装表,保留字形轮廓和编号
// 入参: source PDF字体 // 入参: source PDF字体, type1 已解析的Type1程序,nil时按需解析
// 返回: []byte 封装后的字体数据, error 错误信息 // 返回: []byte 封装后的字体数据, uint16 缺失度量前的完整字形数, error 错误信息
func pdfFontProgram(source *pdfgo.Font) ([]byte, error) { func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, uint16, error) {
program := source.Program program := source.Program
if source.ProgramType == "FontFile" {
if type1 == nil {
parsed, err := parseType1Program(program)
if err != nil {
return nil, 0, err
}
type1 = &parsed
}
var err error
program, err = type1.toCFF(source.Name)
if err != nil {
return nil, 0, err
}
}
bareCFF := len(program) >= 4 && program[0] == 1 && program[1] == 0 && program[2] >= 4 && program[3] >= 1 && program[3] <= 4 bareCFF := len(program) >= 4 && program[0] == 1 && program[1] == 0 && program[2] >= 4 && program[3] >= 1 && program[3] <= 4
if source.ProgramType == "Type1C" || source.ProgramType == "CIDFontType0C" || bareCFF { if source.ProgramType == "Type1C" || source.ProgramType == "CIDFontType0C" || bareCFF {
var err error var err error
program, _, err = wrapCFFToOTF(source.Program) program, _, err = wrapCFFToOTF(program)
if err != nil { if err != nil {
return nil, err return nil, 0, err
} }
} }
tables, err := fontFileTables(program, 0) tables, err := fontFileTables(program, 0)
if err != nil { if err != nil {
return nil, err return nil, 0, err
} }
if len(tables["head"]) < 54 || len(tables["maxp"]) < 6 || len(tables["hhea"]) < 36 || len(tables["hmtx"]) == 0 { if len(tables["head"]) < 54 || len(tables["maxp"]) < 6 || len(tables["hhea"]) < 36 || len(tables["hmtx"]) == 0 {
return nil, fmt.Errorf("embedded PDF font lacks required metrics") return nil, 0, fmt.Errorf("embedded PDF font lacks required metrics")
} }
count := binary.BigEndian.Uint16(tables["maxp"][4:6]) count := binary.BigEndian.Uint16(tables["maxp"][4:6])
if count == 0 { if count == 0 {
return nil, fmt.Errorf("embedded PDF font has no glyphs") return nil, 0, fmt.Errorf("embedded PDF font has no glyphs")
} }
changed := false changed := false
var repairedLimit uint16
metrics := int(binary.BigEndian.Uint16(tables["hhea"][34:36])) metrics := int(binary.BigEndian.Uint16(tables["hhea"][34:36]))
if metrics == 0 || metrics > int(count) { if metrics == 0 || metrics > int(count) {
return nil, fmt.Errorf("invalid embedded PDF font metric count") return nil, 0, fmt.Errorf("invalid embedded PDF font metric count")
} }
metricLength := 4*metrics + 2*(int(count)-metrics) metricLength := 4*metrics + 2*(int(count)-metrics)
if len(tables["hmtx"]) < metricLength { if len(tables["hmtx"]) < metricLength {
return nil, fmt.Errorf("incomplete embedded PDF font metrics") if len(tables["hmtx"]) < 4*metrics || len(tables["hmtx"])%2 != 0 {
return nil, 0, fmt.Errorf("incomplete embedded PDF font metrics: have %d, need %d", len(tables["hmtx"]), metricLength)
}
limit := metrics + (len(tables["hmtx"])-4*metrics)/2
missing, err := pdfMissingLeftBearings(tables, limit, int(count))
if err != nil {
return nil, 0, err
}
tables["hmtx"] = append(bytes.Clone(tables["hmtx"]), missing...)
repairedLimit = uint16(limit)
changed = true
} }
if len(tables["hmtx"]) > metricLength { if len(tables["hmtx"]) > metricLength {
tables["hmtx"] = tables["hmtx"][:metricLength] tables["hmtx"] = tables["hmtx"][:metricLength]
@@ -340,10 +428,10 @@ func pdfFontProgram(source *pdfgo.Font) ([]byte, error) {
for code := range source.Unicode { for code := range source.Unicode {
glyphs, err := source.Decode([]byte(code)) glyphs, err := source.Decode([]byte(code))
if err != nil { if err != nil {
return nil, err return nil, 0, err
} }
if len(glyphs) != 1 || !glyphs[0].HasID { if len(glyphs) != 1 || !glyphs[0].HasID {
return nil, &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 utf8.RuneCountInString(glyph.Text) == 1 { if utf8.RuneCountInString(glyph.Text) == 1 {
@@ -360,7 +448,7 @@ func pdfFontProgram(source *pdfgo.Font) ([]byte, error) {
if len(fontNamesFromTable(tables["name"])) == 0 { if len(fontNamesFromTable(tables["name"])) == 0 {
name := utf16.Encode([]rune(source.Name)) name := utf16.Encode([]rune(source.Name))
if len(name) == 0 || len(name) > 32767 { if len(name) == 0 || len(name) > 32767 {
return nil, fmt.Errorf("PDF font name missing or excessive") return nil, 0, fmt.Errorf("PDF font name missing or excessive")
} }
data := make([]byte, 42+len(name)*2) data := make([]byte, 42+len(name)*2)
binary.BigEndian.PutUint16(data[2:], 3) binary.BigEndian.PutUint16(data[2:], 3)
@@ -394,8 +482,72 @@ func pdfFontProgram(source *pdfgo.Font) ([]byte, error) {
changed = true changed = true
} }
} }
if !changed && len(program)%4 == 0 { if !changed && !pdfSFNTMissingPadding(program) {
return program, nil return program, repairedLimit, nil
} }
return serializeOTF(tables) result, err := serializeOTF(tables)
return result, repairedLimit, err
}
// pdfSFNTMissingPadding 判断字体表目录是否引用了未写入的末尾对齐字节
// 入参: program OpenType字体数据
// 返回: bool 是否需要重新封装
func pdfSFNTMissingPadding(program []byte) bool {
if len(program) < 12 {
return true
}
count := int(binary.BigEndian.Uint16(program[4:6]))
if count > (len(program)-12)/16 {
return true
}
for index := range count {
record := program[12+16*index:]
offset := int(binary.BigEndian.Uint32(record[8:12]))
length := int(binary.BigEndian.Uint32(record[12:16]))
if offset > len(program) || length > len(program)-offset || (4-length%4)%4 > len(program)-offset-length {
return true
}
}
return false
}
// pdfMissingLeftBearings 从轮廓边界恢复缺失的尾部左侧边距
// 入参: tables 字体表, start 首个缺失字形, count 字形总数
// 返回: []byte 补齐的hmtx数据, error 无法读取轮廓
func pdfMissingLeftBearings(tables map[string][]byte, start, count int) ([]byte, error) {
head, loca, glyf := tables["head"], tables["loca"], tables["glyf"]
if len(head) < 54 || len(glyf) == 0 {
return nil, fmt.Errorf("embedded PDF font lacks glyph outlines for missing metrics")
}
format := int(int16(binary.BigEndian.Uint16(head[50:52])))
entrySize := 2
if format == 1 {
entrySize = 4
} else if format != 0 {
return nil, fmt.Errorf("invalid embedded PDF font location format")
}
if len(loca) < (count+1)*entrySize {
return nil, fmt.Errorf("incomplete embedded PDF font glyph locations")
}
location := func(index int) int {
if format == 0 {
return int(binary.BigEndian.Uint16(loca[index*2:])) * 2
}
return int(binary.BigEndian.Uint32(loca[index*4:]))
}
result := make([]byte, 2*(count-start))
for index := start; index < count; index++ {
from, to := location(index), location(index+1)
if from > to || to > len(glyf) {
return nil, fmt.Errorf("invalid embedded PDF font glyph location")
}
if from == to {
continue
}
if to-from < 10 {
return nil, fmt.Errorf("incomplete embedded PDF font glyph outline")
}
copy(result[(index-start)*2:], glyf[from+2:from+4])
}
return result, nil
} }
+6 -3
View File
@@ -656,7 +656,7 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
attrs.add("xmlns:ofd", ofdNamespace) attrs.add("xmlns:ofd", ofdNamespace)
} }
switch object.Type { switch object.Type {
case "TextObject": case "TextObject", "Text":
obj := object.TextObject obj := object.TextObject
attrs.add("ID", obj.ID) attrs.add("ID", obj.ID)
attrs.add("DrawParam", obj.DrawParam) attrs.add("DrawParam", obj.DrawParam)
@@ -746,7 +746,7 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
x.color("StrokeColor", stroke) x.color("StrokeColor", stroke)
x.color("FillColor", fill) x.color("FillColor", fill)
x.text("AbbreviatedData", object.PathObject.AbbreviatedData) x.text("AbbreviatedData", object.PathObject.AbbreviatedData)
} else if object.Type == "TextObject" { } else if object.Type == "TextObject" || object.Type == "Text" {
x.clips(object.TextObject.Clips) x.clips(object.TextObject.Clips)
x.color("FillColor", fill) x.color("FillColor", fill)
x.color("StrokeColor", stroke) x.color("StrokeColor", stroke)
@@ -777,7 +777,7 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
x.end(object.Type) x.end(object.Type)
} }
// clips 写出对象路径裁剪,保留裁剪集合、区域及路径的变换 // clips 写出对象裁剪,保留区域内的路径与文字
// 入参: clips 裁剪集合,nil不输出节点 // 入参: clips 裁剪集合,nil不输出节点
func (x *ofdXML) clips(clips *Clips) { func (x *ofdXML) clips(clips *Clips) {
if clips == nil { if clips == nil {
@@ -796,6 +796,9 @@ func (x *ofdXML) clips(clips *Clips) {
for _, path := range area.Path { for _, path := range area.Path {
x.object(GraphicObject{Type: "Path", PathObject: path}, false) x.object(GraphicObject{Type: "Path", PathObject: path}, false)
} }
for _, text := range area.Text {
x.object(GraphicObject{Type: "Text", TextObject: text}, false)
}
x.end("Area") x.end("Area")
} }
x.end("Clip") x.end("Clip")