mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 13:56:32 +08:00
1 parent
c64bc9d594
commit
35631cdf98
13 files changed
+1188
-76
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-20260925114744-07854fb7628b
|
||||
github.com/xiaoqidun/pdfgo v0.0.0-20260926005801-0f4b07fb311c
|
||||
golang.org/x/crypto v0.57.0
|
||||
golang.org/x/image v0.46.0
|
||||
golang.org/x/text v0.42.0
|
||||
@@ -26,6 +26,7 @@ require (
|
||||
github.com/benoitkugler/textlayout v0.3.2 // indirect
|
||||
github.com/benoitkugler/textprocessing v0.0.6 // 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/scanx v0.0.0-20190309010443-e94503791388 // indirect
|
||||
github.com/tdewolff/minify/v2 v2.24.17 // indirect
|
||||
|
||||
@@ -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/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/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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/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-20260925114744-07854fb7628b h1:4j0AxYMyWpmZLcISwFG4wbNbyRxfb8wDi+j9KlGLybQ=
|
||||
github.com/xiaoqidun/pdfgo v0.0.0-20260925114744-07854fb7628b/go.mod h1:/D0XHgfZ7xNhix95swpNaThpuqoJjVR5wIadogtR5pI=
|
||||
github.com/xiaoqidun/pdfgo v0.0.0-20260926005801-0f4b07fb311c h1:ArBGxLqZghXkkIDtz2UqpsxXPM68qs3qVUqOvW9SkzM=
|
||||
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/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
||||
|
||||
+11
-3
@@ -767,7 +767,7 @@ func (e *Editor) prepareObject(id string, object GraphicObject) (GraphicObject,
|
||||
return cloneEditorObject(object)
|
||||
}
|
||||
|
||||
// validateObjectClips 校验对象的路径裁剪,仅接受无资源引用及嵌套裁剪的路径
|
||||
// validateObjectClips 校验对象的路径与文字裁剪,拒绝嵌套裁剪和无效资源
|
||||
// 入参: clips 裁剪集合,nil表示无裁剪
|
||||
// 返回: 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")
|
||||
}
|
||||
for _, area := range clip.Area {
|
||||
if len(area.Path) == 0 || area.DrawParam != "" || len(area.Text) != 0 {
|
||||
return fmt.Errorf("clips only support paths without draw parameter references")
|
||||
if len(area.Path)+len(area.Text) == 0 || area.DrawParam != "" {
|
||||
return fmt.Errorf("clip area requires paths or text without draw parameter references")
|
||||
}
|
||||
if area.CTM != "" {
|
||||
if _, err := creationNumbers(area.CTM, 6); err != nil {
|
||||
@@ -798,6 +798,14 @@ func (e *Editor) validateObjectClips(clips *Clips) error {
|
||||
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
|
||||
|
||||
+19
-1
@@ -302,23 +302,41 @@ func collectPaintReferences(paint *FillColor, used map[string]bool) {
|
||||
// collectObjectReferences 收集对象和嵌套绘制内容引用的资源
|
||||
// 入参: object 图形对象, used 引用集合
|
||||
func collectObjectReferences(object GraphicObject, used map[string]bool) {
|
||||
var clips *Clips
|
||||
switch object.Type {
|
||||
case "TextObject":
|
||||
case "TextObject", "Text":
|
||||
used[object.TextObject.Font], used[object.TextObject.DrawParam] = true, true
|
||||
collectPaintReferences(object.TextObject.FillColor, used)
|
||||
collectPaintReferences((*FillColor)(object.TextObject.StrokeColor), used)
|
||||
clips = object.TextObject.Clips
|
||||
case "PathObject", "Path":
|
||||
used[object.PathObject.DrawParam] = true
|
||||
collectPaintReferences(object.PathObject.FillColor, used)
|
||||
collectPaintReferences((*FillColor)(object.PathObject.StrokeColor), used)
|
||||
clips = object.PathObject.Clips
|
||||
case "ImageObject":
|
||||
used[object.ImageObject.ResourceID], used[object.ImageObject.ImageMask] = true, true
|
||||
if object.ImageObject.Border != nil {
|
||||
collectPaintReferences((*FillColor)(object.ImageObject.Border.BorderColor), used)
|
||||
}
|
||||
clips = object.ImageObject.Clips
|
||||
case "CompositeObject", "CompositeGraphicUnit":
|
||||
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 校验当前文档的纯色和渐变
|
||||
|
||||
@@ -565,7 +565,7 @@ func (e *Editor) measureCompositeMembers(renderer *Renderer, nodes []*editorComp
|
||||
capability.ReasonCode, capability.Reason = editReason(err), err.Error()
|
||||
errors.As(err, &capability.MissingGlyphs)
|
||||
} 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 {
|
||||
capability.Reflow = capability.TextContent
|
||||
capability.LayoutKnown = text.layout != nil
|
||||
|
||||
@@ -555,6 +555,7 @@ func (e *Editor) objectCapabilities(object GraphicObject, orderable map[*editorX
|
||||
}
|
||||
if object.Type == "TextObject" {
|
||||
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
|
||||
if !all.RewriteText {
|
||||
text := object.TextObject
|
||||
|
||||
+6
-3
@@ -238,8 +238,11 @@ func editorXMLAttributes(node *editorXML, allowed string) bool {
|
||||
func editorXMLSupported(node *editorXML) bool {
|
||||
var allowed, children string
|
||||
switch node.name.Local {
|
||||
case "TextObject":
|
||||
allowed, children = "ID Boundary CTM DrawParam Font Size HScale Weight ReadDirection CharDirection LineWidth MiterLimit Join Italic Visible Stroke Fill Alpha", "Clips FillColor StrokeColor TextCode"
|
||||
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 CGTransform TextCode"
|
||||
case "CGTransform":
|
||||
allowed, children = "CodePosition CodeCount GlyphCount", "Glyphs"
|
||||
case "Glyphs":
|
||||
case "PathObject", "Path":
|
||||
allowed, children = "Boundary CTM LineWidth MiterLimit Join Cap Rule DashPattern DashOffset Visible Stroke Fill Alpha", "StrokeColor FillColor AbbreviatedData"
|
||||
if node.name.Local == "PathObject" {
|
||||
@@ -257,7 +260,7 @@ func editorXMLSupported(node *editorXML) bool {
|
||||
case "Clip":
|
||||
children = "Area"
|
||||
case "Area":
|
||||
allowed, children = "CTM", "Path"
|
||||
allowed, children = "CTM", "Path Text"
|
||||
case "AbbreviatedData":
|
||||
default:
|
||||
return false
|
||||
|
||||
+163
-4
@@ -39,12 +39,13 @@ func wrapCFFToOTF(cffData []byte) ([]byte, map[rune]uint16, error) {
|
||||
if err == nil {
|
||||
cffData = sanitized
|
||||
}
|
||||
cffData, err = normalizeCFFDotsection(cffData)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
widths, err := parseCFFWidths(cffData, numGlyphs)
|
||||
if err != nil {
|
||||
widths = make([]uint16, numGlyphs)
|
||||
for i := range widths {
|
||||
widths[i] = 500
|
||||
}
|
||||
return nil, nil, fmt.Errorf("CFF glyph widths: %w", err)
|
||||
}
|
||||
var unitsPerEm uint16 = 1000
|
||||
if len(cffData) > 4 {
|
||||
@@ -82,6 +83,144 @@ func wrapCFFToOTF(cffData []byte) ([]byte, map[rune]uint16, error) {
|
||||
// cffDict 使用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
|
||||
// 入参: data 原始CFF数据
|
||||
// 返回: []byte 清洗后的CFF数据, error 错误信息
|
||||
@@ -1501,6 +1640,26 @@ func getUnicodeFromName(name string) rune {
|
||||
return '.'
|
||||
case "slash":
|
||||
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":
|
||||
return ':'
|
||||
case "semicolon":
|
||||
|
||||
@@ -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
@@ -48,24 +48,27 @@ type PDFImportReport struct {
|
||||
|
||||
// pdfImporter 保存单次转换的对象与资源状态
|
||||
type pdfImporter struct {
|
||||
ctx context.Context
|
||||
reader *pdfgo.Reader
|
||||
editor *Editor
|
||||
report PDFImportReport
|
||||
matrix pdfgo.Matrix
|
||||
page int
|
||||
fontIDs map[*pdfgo.Font]string
|
||||
fontMetrics map[string]FontMetrics
|
||||
cmykSpace string
|
||||
objects []GraphicObject
|
||||
pages map[pdfgo.Reference]*pdfgo.Page
|
||||
pageIDs map[pdfgo.Reference]string
|
||||
imageIDs map[*pdfgo.Stream]string
|
||||
maskClips map[*pdfgo.SoftMask]pdfgo.Path
|
||||
pageBox pdfgo.Rectangle
|
||||
pageWidth float64
|
||||
pageHeight float64
|
||||
pendingPath *pdfgo.PathMark
|
||||
ctx context.Context
|
||||
reader *pdfgo.Reader
|
||||
editor *Editor
|
||||
report PDFImportReport
|
||||
matrix pdfgo.Matrix
|
||||
page int
|
||||
fontIDs map[*pdfgo.Font]string
|
||||
fontMetrics map[string]FontMetrics
|
||||
fontRepairLimit map[*pdfgo.Font]uint16
|
||||
type1Glyphs map[*pdfgo.Font]map[string]uint16
|
||||
clipTexts map[*pdfgo.TextClip]TextObject
|
||||
cmykSpace string
|
||||
objects []GraphicObject
|
||||
pages map[pdfgo.Reference]*pdfgo.Page
|
||||
pageIDs map[pdfgo.Reference]string
|
||||
imageIDs map[*pdfgo.Stream]string
|
||||
maskClips map[*pdfgo.SoftMask]pdfgo.Path
|
||||
pageBox pdfgo.Rectangle
|
||||
pageWidth float64
|
||||
pageHeight float64
|
||||
pendingPath *pdfgo.PathMark
|
||||
}
|
||||
|
||||
// ImportPDF 将PDF内容转换为独立OFD编辑文档,失败时不返回部分结果
|
||||
@@ -122,6 +125,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
||||
importer.pageWidth = width
|
||||
importer.pageHeight = height
|
||||
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.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
|
||||
return importer.group(mark, walk, visitor)
|
||||
@@ -293,28 +297,44 @@ func (p *pdfImporter) pathData(path pdfgo.Path, origin Box) string {
|
||||
|
||||
// clips 保持裁剪路径的交集,不随对象CTM重复变换
|
||||
// 入参: paths 裁剪路径, origin 对象边界
|
||||
// 返回: *Clips OFD裁剪区域,无裁剪时为空
|
||||
func (p *pdfImporter) clips(paths []pdfgo.Path, origin Box) *Clips {
|
||||
// 返回: *Clips OFD裁剪区域,无裁剪时为空, error 未解析的裁剪字形
|
||||
func (p *pdfImporter) clips(paths []pdfgo.Path, origin Box) (*Clips, error) {
|
||||
if len(paths) == 0 {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
flag := false
|
||||
clips := &Clips{TransFlag: &flag}
|
||||
for _, path := range paths {
|
||||
area := ClipArea{}
|
||||
var points []pdfgo.Point
|
||||
for _, segment := range path.Segments {
|
||||
for _, point := range segment.Points {
|
||||
points = append(points, p.matrix.Apply(point))
|
||||
}
|
||||
}
|
||||
box := pdfBounds(points)
|
||||
rule := "NonZero"
|
||||
if path.EvenOdd {
|
||||
rule = "Even-Odd"
|
||||
if len(path.Segments) != 0 {
|
||||
box := pdfBounds(points)
|
||||
rule := "NonZero"
|
||||
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 合并相同状态下的不透明描边,保留独立子路径与绘制顺序
|
||||
@@ -393,7 +413,11 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
|
||||
return err
|
||||
}
|
||||
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
|
||||
if mark.Path.EvenOdd {
|
||||
object.Rule = "Even-Odd"
|
||||
|
||||
+5
-1
@@ -148,7 +148,11 @@ func (p *pdfImporter) group(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error
|
||||
}
|
||||
region.paint.Alpha = mark.Alpha
|
||||
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.report.PathObjects++
|
||||
}
|
||||
|
||||
+181
-29
@@ -75,20 +75,28 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
|
||||
}
|
||||
if source.ImageMask {
|
||||
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 != [4]float64{} {
|
||||
return &pdfgo.UnsupportedError{Feature: "CMYK stencil image"}
|
||||
}
|
||||
fill = [3]float64{1, 1, 1}
|
||||
components := *mark.Style.Fill.CMYK
|
||||
tint = color.NRGBA64Model.Convert(color.CMYK{
|
||||
C: uint8(math.Round(components[0] * 255)),
|
||||
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))
|
||||
for y := 0; y < source.Height; y++ {
|
||||
for x := 0; x < source.Width; x++ {
|
||||
gray, _, _, _ := decoded.At(x, y).RGBA()
|
||||
stencil.SetNRGBA64(x, y, color.NRGBA64{
|
||||
R: uint16(math.Round(fill[0] * 65535)),
|
||||
G: uint16(math.Round(fill[1] * 65535)),
|
||||
B: uint16(math.Round(fill[2] * 65535)),
|
||||
R: tint.R,
|
||||
G: tint.G,
|
||||
B: tint.B,
|
||||
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})
|
||||
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))
|
||||
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.report.ImageObjects++
|
||||
return nil
|
||||
@@ -130,6 +142,9 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
|
||||
// 返回: error 错误信息
|
||||
func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
||||
if mark.Font.Subtype == pdfgo.Name("Type3") {
|
||||
if mark.Clip != nil {
|
||||
return &pdfgo.UnsupportedError{Feature: "Type3 text clipping"}
|
||||
}
|
||||
if err := p.flushPath(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -155,23 +170,44 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
||||
if err != nil {
|
||||
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"}
|
||||
}
|
||||
font := mark.Font
|
||||
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)}
|
||||
}
|
||||
id := p.fontIDs[font]
|
||||
if id == "" {
|
||||
var err error
|
||||
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
|
||||
program, err = pdfFontProgram(font)
|
||||
var limit uint16
|
||||
program, limit, err = pdfFontProgram(font, type1)
|
||||
if err != nil {
|
||||
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)
|
||||
} else {
|
||||
id, err = p.editor.AddExternalFont(font.Name)
|
||||
@@ -181,6 +217,13 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
||||
}
|
||||
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 outline FontOutlines
|
||||
if embedded {
|
||||
@@ -208,6 +251,14 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
||||
position := 0
|
||||
for n, glyph := range mark.Glyphs {
|
||||
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 {
|
||||
character := getUnicodeFromName(glyph.Name)
|
||||
if character == 0 {
|
||||
@@ -256,7 +307,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
||||
}
|
||||
}
|
||||
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 {
|
||||
scale := math.Sqrt(math.Abs(m[0]*m[3] - m[1]*m[2]))
|
||||
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 {
|
||||
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.report.TextObjects++
|
||||
return nil
|
||||
}
|
||||
|
||||
// pdfFontProgram 为PDF子集字体补齐封装表,保留字形轮廓和编号
|
||||
// 入参: source PDF字体
|
||||
// 返回: []byte 封装后的字体数据, error 错误信息
|
||||
func pdfFontProgram(source *pdfgo.Font) ([]byte, error) {
|
||||
// 入参: source PDF字体, type1 已解析的Type1程序,nil时按需解析
|
||||
// 返回: []byte 封装后的字体数据, uint16 缺失度量前的完整字形数, error 错误信息
|
||||
func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, uint16, error) {
|
||||
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
|
||||
if source.ProgramType == "Type1C" || source.ProgramType == "CIDFontType0C" || bareCFF {
|
||||
var err error
|
||||
program, _, err = wrapCFFToOTF(source.Program)
|
||||
program, _, err = wrapCFFToOTF(program)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, 0, err
|
||||
}
|
||||
}
|
||||
tables, err := fontFileTables(program, 0)
|
||||
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 {
|
||||
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])
|
||||
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
|
||||
var repairedLimit uint16
|
||||
metrics := int(binary.BigEndian.Uint16(tables["hhea"][34:36]))
|
||||
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)
|
||||
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 {
|
||||
tables["hmtx"] = tables["hmtx"][:metricLength]
|
||||
@@ -340,10 +428,10 @@ func pdfFontProgram(source *pdfgo.Font) ([]byte, error) {
|
||||
for code := range source.Unicode {
|
||||
glyphs, err := source.Decode([]byte(code))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, 0, err
|
||||
}
|
||||
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]
|
||||
if utf8.RuneCountInString(glyph.Text) == 1 {
|
||||
@@ -360,7 +448,7 @@ func pdfFontProgram(source *pdfgo.Font) ([]byte, error) {
|
||||
if len(fontNamesFromTable(tables["name"])) == 0 {
|
||||
name := utf16.Encode([]rune(source.Name))
|
||||
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)
|
||||
binary.BigEndian.PutUint16(data[2:], 3)
|
||||
@@ -394,8 +482,72 @@ func pdfFontProgram(source *pdfgo.Font) ([]byte, error) {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if !changed && len(program)%4 == 0 {
|
||||
return program, nil
|
||||
if !changed && !pdfSFNTMissingPadding(program) {
|
||||
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
@@ -656,7 +656,7 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
|
||||
attrs.add("xmlns:ofd", ofdNamespace)
|
||||
}
|
||||
switch object.Type {
|
||||
case "TextObject":
|
||||
case "TextObject", "Text":
|
||||
obj := object.TextObject
|
||||
attrs.add("ID", obj.ID)
|
||||
attrs.add("DrawParam", obj.DrawParam)
|
||||
@@ -746,7 +746,7 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
|
||||
x.color("StrokeColor", stroke)
|
||||
x.color("FillColor", fill)
|
||||
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.color("FillColor", fill)
|
||||
x.color("StrokeColor", stroke)
|
||||
@@ -777,7 +777,7 @@ func (x *ofdXML) object(object GraphicObject, root bool) {
|
||||
x.end(object.Type)
|
||||
}
|
||||
|
||||
// clips 写出对象路径裁剪,保留裁剪集合、区域及路径的变换
|
||||
// clips 写出对象裁剪,保留区域内的路径与文字
|
||||
// 入参: clips 裁剪集合,nil不输出节点
|
||||
func (x *ofdXML) clips(clips *Clips) {
|
||||
if clips == nil {
|
||||
@@ -796,6 +796,9 @@ func (x *ofdXML) clips(clips *Clips) {
|
||||
for _, path := range area.Path {
|
||||
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("Clip")
|
||||
|
||||
Reference in new issue
Block a user