// 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 ( "math" "reflect" "strings" "unicode/utf8" codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) // pdfTextReplacement 记录一个完整替换区段的文字数量与转换状态 type pdfTextReplacement struct { text *pdfgo.ReplacementText count int matched int offset int64 blocked bool applied bool identity bool } // pdfTextReplacements 按绘制顺序收集替换区段,避免拆分或重复写入原文 type pdfTextReplacements struct { importer *pdfImporter spans map[*pdfgo.ReplacementText]*pdfTextReplacement order []*pdfTextReplacement widget bool } // span 查找最外层替换区段,相邻区段即使原文相同也分别保留 // 入参: text 替换文本 // 返回: *pdfTextReplacement 区段状态,未声明时为空 func (r *pdfTextReplacements) span(text *pdfgo.ReplacementText) *pdfTextReplacement { if text == nil { return nil } if r.spans == nil { r.spans = make(map[*pdfgo.ReplacementText]*pdfTextReplacement) } first := text for text.Parent != nil && r.spans[text] == nil { text = text.Parent } span := r.spans[text] if span == nil { span = &pdfTextReplacement{text: text} r.spans[text] = span r.order = append(r.order, span) } for first != text { r.spans[first] = span first = first.Parent } return span } // block 标记无法仅用字符与字形变换表达的区段 // 入参: text 替换文本 func (r *pdfTextReplacements) block(text *pdfgo.ReplacementText) { if span := r.span(text); span != nil { span.blocked = true } } // markedContent 处理结构区段、可选内容及替换文本属性 // 入参: mark 内容标记 // 返回: error 未支持的语义错误 func (r *pdfTextReplacements) markedContent(mark pdfgo.MarkedContentMark) error { if mark.Operator == "EMC" { return nil } if r.importer.structureBindings != nil { if err := r.importer.prepareSourceStructure(r.importer.ctx, mark.Reader); err != nil { return err } r.importer.structureBindings.observe(mark.Sequence) if mark.Sequence != nil && r.importer.structureBindings.marked != nil { key, ok, err := pdfStructureContentKey(mark.Sequence) if err != nil { return err } if ok && r.importer.structureBindings.marked[key] { return nil } } } if mark.Tag == "OC" { kind, err := mark.Reader.Resolve(mark.Properties["Type"]) if err != nil { return err } if kind == pdfgo.Name("OCG") || kind == pdfgo.Name("OCMD") { return nil } } if r.widget && mark.Tag == "Tx" && len(mark.Properties) == 0 { return nil } if mark.Replacement != nil && len(mark.Properties) == 1 { return nil } if r.importer.warning == nil { return &pdfgo.UnsupportedError{Feature: "marked content requiring semantic preservation"} } r.importer.warning(pdfgo.Diagnostic{Offset: mark.Offset, Message: "marked content " + string(mark.Tag) + " not preserved"}) return nil } // apply 合并可等价表示的替换区段,原文相同但样式不同的区段保留原图元 // 入参: nodes 原始图元 // 返回: []pdfCompositeNode 转换图元, error 取消或语义转换错误 func (r *pdfTextReplacements) apply(nodes []pdfCompositeNode) ([]pdfCompositeNode, error) { if len(r.order) == 0 { return nodes, nil } if err := r.count(nodes, false); err != nil { return nil, err } for _, span := range r.order { span.identity = !span.blocked && span.count > 0 && span.matched == len(span.text.Text) } var err error nodes, err = r.merge(nodes) if err != nil { return nil, err } for _, span := range r.order { if span.applied || span.identity || !span.blocked && span.count == 0 && span.text.Text == "" { continue } if r.importer.warning == nil { return nil, &pdfgo.UnsupportedError{Feature: "ActualText span without equivalent OFD glyph mapping"} } r.importer.warning(pdfgo.Diagnostic{Offset: span.offset, Message: "PDF ActualText span not preserved; original appearance retained"}) } r.clear(nodes) return nodes, nil } // count 统计区段文字并逐字核对原文,排除需要组级合成的内容 // 入参: nodes 图元, blocked 上层组是否阻止直接转换 // 返回: error 取消错误 func (r *pdfTextReplacements) count(nodes []pdfCompositeNode, blocked bool) error { for _, node := range nodes { if err := r.importer.ctx.Err(); err != nil { return err } if node.text != nil { if span := r.span(node.text.Replacement); span != nil { span.count++ span.blocked = span.blocked || blocked for i, glyph := range node.text.Glyphs { if span.matched < 0 { break } if i%256 == 0 { if err := r.importer.ctx.Err(); err != nil { return err } } if glyph.Text == "" || !strings.HasPrefix(span.text.Text[span.matched:], glyph.Text) { span.matched = -1 break } span.matched += len(glyph.Text) } } } childBlocked := blocked if group := node.group; group != nil && node.textObject == nil { opaque, err := r.importer.opaque(node, group.ColorSpace) if err := r.importer.ctx.Err(); err != nil { return err } childBlocked = childBlocked || !opaque || err != nil || group.ColorSpace != nil && !group.ColorSpace.SRGBEquivalent() } if err := r.count(node.children, childBlocked); err != nil { return err } } return nil } // merge 合并同层连续图元,保留原始绘制顺序与字形坐标 // 入参: nodes 图元 // 返回: []pdfCompositeNode 合并结果, error 取消错误 func (r *pdfTextReplacements) merge(nodes []pdfCompositeNode) ([]pdfCompositeNode, error) { result := nodes[:0] for i := 0; i < len(nodes); { if err := r.importer.ctx.Err(); err != nil { return nil, err } node := nodes[i] var err error node.children, err = r.merge(node.children) if err != nil { return nil, err } end := i + 1 combined := false first := node.text if first == nil && node.textObject != nil && len(node.children) != 0 { first = node.children[0].text } if first != nil { if span := r.span(first.Replacement); span != nil && !span.applied && !span.blocked && span.text.Text != "" && !(span.identity && span.count == 1) { var marks []pdfCompositeNode for end = i; end < len(nodes); end++ { current := nodes[end] children := current.children if current.text != nil { children = nodes[end : end+1] } else if current.textObject == nil { break } valid := len(children) != 0 for _, child := range children { if child.text == nil || r.span(child.text.Replacement) != span { valid = false break } } if !valid { break } marks = append(marks, children...) } end = max(end, i+1) if len(marks) == span.count { if mark, ok := pdfMergeReplacement(marks, span.text); ok { var origins pdfOriginSet for _, source := range marks { if err := r.importer.ctx.Err(); err != nil { return nil, err } origin := r.importer.textOrigin(*source.text) origins.add(origin.sequence) for _, sequence := range origin.related { origins.add(sequence) } } if len(origins.origin.related) != 0 { if r.importer.textOrigins == nil { r.importer.textOrigins = make(map[*pdfgo.ReplacementText]pdfObjectOrigin) } r.importer.textOrigins[span.text] = origins.origin } if node.text != nil { node.text = mark } else { node.children = []pdfCompositeNode{{text: mark}} } span.applied = true combined = true } } } } node.formCount = 0 result = append(result, node) if !combined { for _, remaining := range nodes[i+1 : end] { remaining.formCount = 0 result = append(result, remaining) } } i = end } clear(nodes[len(result):]) return result, nil } // clear 清除无需替换及未转换的区段引用,防止局部绘制重复使用整段原文 // 入参: nodes 图元 func (r *pdfTextReplacements) clear(nodes []pdfCompositeNode) { for _, node := range nodes { if node.text != nil { if span := r.span(node.text.Replacement); span != nil && !span.applied { node.text.Replacement = nil } } r.clear(node.children) } } // textOrigin 获取合并文字的完整来源,普通文字沿用原始区段 // 入参: mark 文字图元 // 返回: pdfObjectOrigin 来源区段 func (p *pdfImporter) textOrigin(mark pdfgo.TextMark) pdfObjectOrigin { if origin, ok := p.textOrigins[mark.Replacement]; ok { return origin } return pdfObjectOrigin{sequence: mark.Sequence} } // pdfMergeReplacement 统一同一替换区段内的坐标原点,不重新塑形或调整字距 // 入参: nodes 连续文字图元, replacement 整段原文 // 返回: *pdfgo.TextMark 合并文字, bool 是否可等价合并 func pdfMergeReplacement(nodes []pdfCompositeNode, replacement *pdfgo.ReplacementText) (*pdfgo.TextMark, bool) { first := nodes[0].text style := first.Style paint := style.Fill if first.Font == nil || len(first.Font.Program) == 0 || first.Mode != 0 || first.Size <= 0 || first.HorizontalScale <= 0 || first.Clip != nil || style.SoftMask != nil || style.Transfer != nil || style.AlphaIsShape || style.FillOverprint || paint.Alpha != 1 || !codecpdf.NormalBlend(style.BlendMode) || paint.Tiling != nil || paint.Axial != nil || paint.Radial != nil || paint.Mesh != nil || paint.Function != nil { return nil, false } inverse, ok := first.Matrix.Inverse() if !ok { return nil, false } count := 0 sequence := first.Sequence for _, node := range nodes { mark := node.text if mark.Font != first.Font || mark.Size != first.Size || mark.HorizontalScale != first.HorizontalScale || mark.Mode != first.Mode || mark.Clip != nil || len(mark.Glyphs) != len(mark.Positions) || !reflect.DeepEqual(mark.Style, style) { return nil, false } if mark.Matrix[0] != first.Matrix[0] || mark.Matrix[1] != first.Matrix[1] || mark.Matrix[2] != first.Matrix[2] || mark.Matrix[3] != first.Matrix[3] { return nil, false } count += len(mark.Glyphs) sequence = pdfCommonSequence(sequence, mark.Sequence) } if count == 0 { return nil, false } merged := *first merged.Replacement = replacement merged.Sequence = sequence merged.Glyphs = make([]pdfgo.Glyph, 0, count) merged.Positions = make([]pdfgo.Point, 0, count) for _, node := range nodes { mark := node.text dx, dy := mark.Matrix[4]-first.Matrix[4], mark.Matrix[5]-first.Matrix[5] offset := pdfgo.Point{X: inverse[0]*dx + inverse[2]*dy, Y: inverse[1]*dx + inverse[3]*dy} for _, point := range mark.Positions { point.X += offset.X point.Y += offset.Y if math.IsNaN(point.X) || math.IsNaN(point.Y) || math.IsInf(point.X, 0) || math.IsInf(point.Y, 0) { return nil, false } merged.Positions = append(merged.Positions, point) } merged.Glyphs = append(merged.Glyphs, mark.Glyphs...) } return &merged, true } // pdfCommonSequence 查找两个内容区段的最近共同外层,不将合并内容归入单个子区段 // 入参: first 首个区段, second 后续区段 // 返回: *pdfgo.ContentSequence 共同区段,没有共同区段时为空 func pdfCommonSequence(first, second *pdfgo.ContentSequence) *pdfgo.ContentSequence { if first == second { return first } if first == nil || second == nil { return nil } depth := func(sequence *pdfgo.ContentSequence) int { count := 0 for ; sequence != nil; sequence = sequence.Parent { count++ } return count } a, b := depth(first), depth(second) for ; a > b; a-- { first = first.Parent } for ; b > a; b-- { second = second.Parent } for first != second { first, second = first.Parent, second.Parent } return first } // pdfApplyReplacement 将整段原文映射到全部字形,字形位移遵循GB/T 33190的文字定位规则 // 入参: object 文字对象, text 整段原文, positions 原始字形位置 func pdfApplyReplacement(object *TextObject, text string, positions []pdfgo.Point) { const unit = 25.4 / 72 glyphs := make([]string, len(object.CGTransform)) for i, transform := range object.CGTransform { glyphs[i] = transform.Glyphs } dx, dy := make([]float64, len(positions)-1), make([]float64, len(positions)-1) for i := 1; i < len(positions); i++ { dx[i-1] = (positions[i].X - positions[i-1].X) * unit dy[i-1] = -(positions[i].Y - positions[i-1].Y) * unit } object.TextCode = []TextCode{{X: pdfNumbers(positions[0].X * unit), Y: pdfNumbers(-positions[0].Y * unit), DeltaX: pdfNumbers(dx...), DeltaY: pdfNumbers(dy...), Value: escapeOFDText(text)}} object.CGTransform = []CGTransform{{CodeCount: utf8.RuneCountInString(text), GlyphCount: len(glyphs), Glyphs: strings.Join(glyphs, " ")}} }