// 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 ( "fmt" "slices" "github.com/tdewolff/canvas" canvastext "github.com/tdewolff/canvas/text" basefont "github.com/xiaoqidun/ofdgo/internal/base/font" codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf" "github.com/xiaoqidun/pdfgo" ) // pdfFontSubset 保存独立CFF输出字体及实际用字,包含缺省字形 type pdfFontSubset struct { font *canvas.Font glyphs map[uint16]bool } // RenderText 记录后端文字区段,写出时校验数量并关联原文 // 入参: text 画布文字, matrix 文字变换 func (r *pdfRenderer) RenderText(text *canvas.Text, matrix canvas.Matrix) { if len(r.text) == 0 { r.text = append(r.text, codecpdf.TextPage{}) } page := &r.text[len(r.text)-1] text.WalkSpans(func(_, _ float64, span canvas.TextSpan) { if span.IsText() { page.Count++ } }) r.PDF.RenderText(text, matrix) } // preserveText 将一个OFD文字对象关联到完整PDF文字区段 // 入参: first 起始文字对象序号, text 原文 func (r *pdfRenderer) preserveText(first int, text string) { page := &r.text[len(r.text)-1] if page.Count > first { page.Replacements = append(page.Replacements, pdfgo.TextReplacement{First: first, Count: page.Count - first, Text: text}) r.exactText = true } } // glyphText 使用独立输出字体和既定字形建立PDF文字,不改变字体缓存及定位 // 入参: face 字体, glyph 字形 // 返回: *canvas.Text 画布文字, error 字体解析错误 func (r *pdfRenderer) glyphText(face *canvas.FontFace, glyph textGlyph) (*canvas.Text, error) { font := r.fonts[face.Font] if font == nil { loaded, err := loadCanvasFont(basefont.SFNTData(face.Font.Tables), 0, face.Font.Style()) if err != nil { return nil, err } copy := *face.Font copy.SFNT = loaded.SFNT font = © if r.fonts == nil { r.fonts = make(map[*canvas.Font]*canvas.Font) } r.fonts[face.Font] = font } outputFace := *face outputFace.Font = font face = &outputFace var text *canvas.Text if glyph.GlyphID < 0 || glyph.GlyphID > 0xffff { text = canvas.NewTextLine(face, glyph.Text, canvas.Left) } else { id := uint16(glyph.GlyphID) text = canvas.NewTextLine(face, " ", canvas.Left) text.WalkLines(func(_ float64, spans []canvas.TextSpan) { for i := range spans { spans[i].Text = glyph.Text spans[i].Glyphs = []canvastext.Glyph{{SFNT: face.Font.SFNT, Size: face.Size, ID: id, XAdvance: int32(face.Font.GlyphAdvance(id)), Text: glyph.Text}} } }) } if font.IsCFF { if err := r.cffTextFont(font, text); err != nil { return nil, err } } return text, nil } // cffTextFont 按实际用字隔离CFF子集,避免后端清除字形名后超出ISOAdobe字符集容量 // 入参: font 独立输出字体, text 已定位文字 // 返回: error 字形数量或字体解析错误 func (r *pdfRenderer) cffTextFont(font *canvas.Font, text *canvas.Text) error { const limit = 229 ids := []uint16{0} text.WalkSpans(func(_, _ float64, span canvas.TextSpan) { for _, glyph := range span.Glyphs { if !slices.Contains(ids, glyph.ID) { ids = append(ids, glyph.ID) } } }) if len(ids) > limit { return fmt.Errorf("CFF text glyph count exceeds output subset capacity") } var selected *pdfFontSubset for _, subset := range r.cffFonts[font] { count := len(subset.glyphs) for _, id := range ids { if !subset.glyphs[id] { count++ } } if count <= limit { selected = subset break } } if selected == nil { output := font if len(r.cffFonts[font]) != 0 { loaded, err := loadCanvasFont(basefont.SFNTData(font.Tables), 0, font.Style()) if err != nil { return err } copy := *font copy.SFNT = loaded.SFNT output = © } selected = &pdfFontSubset{font: output, glyphs: make(map[uint16]bool)} if r.cffFonts == nil { r.cffFonts = make(map[*canvas.Font][]*pdfFontSubset) } r.cffFonts[font] = append(r.cffFonts[font], selected) } for _, id := range ids { selected.glyphs[id] = true } if selected.font != font { text.WalkLines(func(_ float64, spans []canvas.TextSpan) { for i := range spans { face := *spans[i].Face face.Font = selected.font spans[i].Face = &face for j := range spans[i].Glyphs { spans[i].Glyphs[j].SFNT = selected.font.SFNT } } }) } return nil } // pdfTextSupported 核对原生文字的字形和未裁剪范围,复用实际绘制的字形缓存 // 入参: object 文字对象, fontID 字体标识, face 实际字体, transforms 字形映射, clip 裁剪路径, height 页面高度, matrix 对象变换, glyphMatrix 字形变换 // 返回: bool 是否能完整保留字形外观 func (r *Renderer) pdfTextSupported(object TextObject, fontID string, face *canvas.FontFace, transforms map[int]textGlyphTransform, clip *canvas.Path, height float64, matrix Matrix, glyphMatrix canvas.Matrix) bool { if face.Font.IsCFF && face.Font.CFF == nil { return false } var rect canvas.Rect if clip != nil { var ok bool rect, ok = rectangularPath(clip) if !ok { return false } const tolerance = 1e-9 rect = canvas.Rect{X0: rect.X0 - tolerance, Y0: rect.Y0 - tolerance, X1: rect.X1 + tolerance, Y1: rect.Y1 + tolerance} } scale := object.HScale if scale == 0 { scale = 1 } position := 0 for _, code := range object.TextCode { runes := textCodeRunes(code.Value) glyphs := textCodeGlyphs(runes, transforms, position) if code.Index != "" { runes = r.parseIndexRunes(code.Index, fontID) glyphs = textRuneGlyphs(runes) } positioner := NewTextPositioner(code) for _, glyph := range glyphs { if glyph.GlyphID == 0 || glyph.GlyphID >= int(face.Font.NumGlyphs()) { return false } if glyph.GlyphID < 0 { for _, char := range glyph.Text { if face.Font.GlyphIndex(char) == 0 { return false } } } origin := positioner.Next() if clip == nil { continue } path, _ := r.cachedTextGlyphPath(face, glyph) if path == nil || r.renderError != nil { return false } if path.Empty() { continue } x, y := matrix.Transform(origin.X, origin.Y) view := canvas.Identity.Translate(x, height-y).Mul(glyphMatrix).Scale(scale, 1) if !rect.Contains(path.Copy().Transform(view).Bounds()) { return false } } position += len(runes) } return true }