diff --git a/go.mod b/go.mod index 1f89e29..131561c 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index d57b640..eb8eec1 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/ofdgo_editor.go b/ofdgo_editor.go index d0586eb..00d0b7f 100644 --- a/ofdgo_editor.go +++ b/ofdgo_editor.go @@ -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 diff --git a/ofdgo_editor_color.go b/ofdgo_editor_color.go index 131ebd6..c553ad5 100644 --- a/ofdgo_editor_color.go +++ b/ofdgo_editor_color.go @@ -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 校验当前文档的纯色和渐变 diff --git a/ofdgo_editor_composite.go b/ofdgo_editor_composite.go index 090342f..5006b2d 100644 --- a/ofdgo_editor_composite.go +++ b/ofdgo_editor_composite.go @@ -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 diff --git a/ofdgo_editor_open.go b/ofdgo_editor_open.go index f373a1f..1fd840f 100644 --- a/ofdgo_editor_open.go +++ b/ofdgo_editor_open.go @@ -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 diff --git a/ofdgo_editor_xml.go b/ofdgo_editor_xml.go index 113b884..edc7e89 100644 --- a/ofdgo_editor_xml.go +++ b/ofdgo_editor_xml.go @@ -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 diff --git a/ofdgo_font_cff.go b/ofdgo_font_cff.go index ee0ddce..bb81b2e 100644 --- a/ofdgo_font_cff.go +++ b/ofdgo_font_cff.go @@ -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": diff --git a/ofdgo_font_type1.go b/ofdgo_font_type1.go new file mode 100644 index 0000000..4ee13ee --- /dev/null +++ b/ofdgo_font_type1.go @@ -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 +} diff --git a/ofdgo_pdf.go b/ofdgo_pdf.go index 22f8db9..5dce6d1 100644 --- a/ofdgo_pdf.go +++ b/ofdgo_pdf.go @@ -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" diff --git a/ofdgo_pdf_group.go b/ofdgo_pdf_group.go index 3084ea1..b1aa86c 100644 --- a/ofdgo_pdf_group.go +++ b/ofdgo_pdf_group.go @@ -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++ } diff --git a/ofdgo_pdf_object.go b/ofdgo_pdf_object.go index f9a5501..6ebb72e 100644 --- a/ofdgo_pdf_object.go +++ b/ofdgo_pdf_object.go @@ -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 } diff --git a/ofdgo_writer.go b/ofdgo_writer.go index f9ef74c..17b6cdf 100644 --- a/ofdgo_writer.go +++ b/ofdgo_writer.go @@ -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")