diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 3099d98..8b5ecb4 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -53,6 +53,7 @@ const state = { continuous: false, renderAnnotations: true, pageCache: new Map(), + svgFonts: new Map(), selectedPages: new Set(), pageInFlight: new Map(), pageRenderQueue: [], @@ -1509,6 +1510,10 @@ function renderPageFlow() { } function resetPageFlow() { + for (const font of state.svgFonts.values()) { + document.fonts.delete(font); + } + state.svgFonts.clear(); state.pageCache.clear(); state.selectedPages.clear(); state.visiblePages.clear(); @@ -1657,6 +1662,7 @@ async function processPageRenderQueue() { continue; } const page = await callWASM("ofdgoRenderPage", task.index); + await loadSVGFonts(page.fonts, task.openSeq); if (task.openSeq === state.openSeq) { state.pageCache.set(task.index, page); } @@ -1672,6 +1678,28 @@ async function processPageRenderQueue() { } } +async function loadSVGFonts(fonts, openSeq) { + for (const font of fonts) { + if (openSeq !== state.openSeq) { + return; + } + if (state.svgFonts.has(font.name)) { + continue; + } + const data = await callWASM("ofdgoSVGFontData", font.name); + if (openSeq !== state.openSeq) { + return; + } + const face = new FontFace(font.name, data.bytes, { weight: String(font.weight), style: font.style }); + await face.load(); + if (openSeq !== state.openSeq) { + return; + } + document.fonts.add(face); + state.svgFonts.set(font.name, face); + } +} + function comparePageRenderTask(a, b) { return a.priority - b.priority || Math.abs(a.index - state.pageIndex) - Math.abs(b.index - state.pageIndex); } diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go index 2f76dc4..b72c1e0 100644 --- a/internal/webui/wasm.go +++ b/internal/webui/wasm.go @@ -39,6 +39,7 @@ func RunWASM() { registerCallback("ofdgoDocumentInfo", documentInfo) registerCallback("ofdgoAttachmentData", attachmentData) registerCallback("ofdgoRenderPage", renderPage) + registerCallback("ofdgoSVGFontData", svgFontData) registerCallback("ofdgoSearchPage", searchPage) registerCallback("ofdgoExportFormats", exportFormats) registerCallback("ofdgoExportPage", exportPage) @@ -150,6 +151,20 @@ func attachmentData(args []js.Value) (any, error) { return successResult(map[string]any{"bytes": bytesToJS(data)}), nil } +// svgFontData 读取SVG字体资源 +// 入参: args 浏览器参数 +// 返回: any 字体数据, error 错误信息 +func svgFontData(args []js.Value) (any, error) { + if currentSession == nil { + return nil, fmt.Errorf("ofd document is not opened") + } + data, err := currentSession.SVGFontData(args[0].String()) + if err != nil { + return nil, err + } + return successResult(map[string]any{"bytes": bytesToJS(data)}), nil +} + // renderPage 渲染OFD页面 // 入参: args 浏览器参数 // 返回: any 页面SVG结果, error 错误信息 @@ -182,6 +197,10 @@ func renderPage(args []js.Value) (any, error) { "height": link.Box.H, } } + fonts := make([]any, len(page.Fonts)) + for i, font := range page.Fonts { + fonts[i] = map[string]any{"name": font.Name, "weight": font.Weight, "style": font.Style} + } return successResult(map[string]any{ "index": page.Index, "number": page.Number, @@ -190,6 +209,7 @@ func renderPage(args []js.Value) (any, error) { "height": page.Height, "svg": page.SVG, "links": links, + "fonts": fonts, "text": js.Global().Get("JSON").Call("parse", string(textData)), }), nil } diff --git a/internal/webui/webui.go b/internal/webui/webui.go index c0cc3c5..255bb62 100644 --- a/internal/webui/webui.go +++ b/internal/webui/webui.go @@ -108,6 +108,7 @@ type Session struct { pageCache map[int]*ofdgo.PageContent boxCache map[int]ofdgo.Box textCache map[int]*ofdgo.PageText + svgFonts map[string][]byte signatures []SignatureInfo signatureError error signaturesRead bool @@ -165,6 +166,7 @@ type PageSVG struct { Height float64 `json:"height"` SVG string `json:"svg"` Links []ofdgo.PageLink `json:"-"` + Fonts []ofdgo.SVGFont `json:"-"` } // ExportFormat 导出格式 @@ -241,6 +243,7 @@ func Open(data []byte, opts OpenOptions) (*Session, error) { pageCache: make(map[int]*ofdgo.PageContent), boxCache: make(map[int]ofdgo.Box), textCache: make(map[int]*ofdgo.PageText), + svgFonts: make(map[string][]byte), }, nil } @@ -267,6 +270,7 @@ func (s *Session) SetFonts(fonts []FontFile) error { s.Renderer.SetFontFS() } clear(s.textCache) + clear(s.svgFonts) return nil } @@ -415,14 +419,30 @@ func (s *Session) RenderPageSVG(index int) (PageSVG, error) { return PageSVG{}, err } var buf bytes.Buffer - if err := s.Renderer.RenderToSVG(page, &buf); err != nil { + fonts, err := s.Renderer.RenderToSVGWithFonts(page, &buf) + if err != nil { return PageSVG{}, err } links, err := s.Renderer.PageLinks(page) if err != nil { return PageSVG{}, err } - return PageSVG{Index: index, Number: index + 1, ID: pageRef.ID, Width: box.W, Height: box.H, SVG: buf.String(), Links: links}, nil + for i, font := range fonts { + s.svgFonts[font.Name] = font.Data + fonts[i].Data = nil + } + return PageSVG{Index: index, Number: index + 1, ID: pageRef.ID, Width: box.W, Height: box.H, SVG: buf.String(), Links: links, Fonts: fonts}, nil +} + +// SVGFontData 获取已渲染页面引用的字体数据 +// 入参: name 字体资源标识 +// 返回: []byte 字体数据, error 错误信息 +func (s *Session) SVGFontData(name string) ([]byte, error) { + data, ok := s.svgFonts[name] + if !ok { + return nil, fmt.Errorf("svg font not found: %s", name) + } + return data, nil } // ExportPage 导出单页 diff --git a/ofdgo_renderer.go b/ofdgo_renderer.go index d28cde0..bdee388 100644 --- a/ofdgo_renderer.go +++ b/ofdgo_renderer.go @@ -35,6 +35,7 @@ type Renderer struct { FontGIDMap map[string]map[uint16]rune FontCIDMap map[string]map[uint16]rune fontCache map[fontCacheKey]*canvas.FontFamily + svgFontCache map[*canvas.Font]SVGFont fontSourceCache map[string][]fontSource fontSourceUsed map[string]fontSource fontDirCandidates map[string][]fontFileCandidate @@ -64,6 +65,7 @@ func (r *Renderer) resetFontCache() { r.FontGIDMap = make(map[string]map[uint16]rune) r.FontCIDMap = make(map[string]map[uint16]rune) r.fontCache = make(map[fontCacheKey]*canvas.FontFamily) + r.svgFontCache = make(map[*canvas.Font]SVGFont) r.fontSourceCache = make(map[string][]fontSource) r.fontSourceUsed = make(map[string]fontSource) r.fontDirCandidates = make(map[string][]fontFileCandidate) diff --git a/ofdgo_renderer_svg.go b/ofdgo_renderer_svg.go new file mode 100644 index 0000000..b428e7c --- /dev/null +++ b/ofdgo_renderer_svg.go @@ -0,0 +1,93 @@ +// 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 ( + "crypto/sha256" + "fmt" + "io" + "strings" + + "github.com/tdewolff/canvas" + "github.com/tdewolff/canvas/renderers/svg" +) + +// SVGFont SVG引用的字体资源,Name为CSS字体族标识,Data为只读字体数据 +type SVGFont struct { + Name string + Weight int + Style string + Data []byte +} + +// svgFontRenderer 分离字体资源的SVG渲染器 +type svgFontRenderer struct { + *svg.SVG + renderer *Renderer + fonts []SVGFont + seen map[string]bool + styles strings.Builder +} + +// RenderToSVGWithFonts 渲染为SVG并返回页面引用的字体资源,不在SVG中嵌入字体 +// 入参: page 页面内容, writer 输出流 +// 返回: []SVGFont 字体资源,可按Name复用并按Weight、Style注册,error 错误信息 +func (r *Renderer) RenderToSVGWithFonts(page *PageContent, writer io.Writer) ([]SVGFont, error) { + c, err := r.renderPage(page) + if err != nil { + return nil, err + } + options := svg.DefaultOptions + options.EmbedFonts = false + s := &svgFontRenderer{ + SVG: svg.New(writer, c.W, c.H, &options), + renderer: r, + seen: make(map[string]bool), + } + c.RenderTo(s) + s.SetCustomStyle(s.styles.String()) + return s.fonts, s.Close() +} + +// RenderText 绘制文字并记录实际使用的字体资源 +// 入参: text 文字对象, m 变换矩阵 +func (s *svgFontRenderer) RenderText(text *canvas.Text, m canvas.Matrix) { + if text.Empty() { + return + } + font := text.MostCommonFontFace().Font + resource, ok := s.renderer.svgFontCache[font] + if !ok { + data := font.SFNT.Write() + resource = SVGFont{ + Name: fmt.Sprintf("ofdgo-%x-%d", sha256.Sum256(data), font.Style()), + Weight: font.Style().CSS(), + Style: "normal", + Data: data, + } + if font.Style().Italic() { + resource.Style = "italic" + } + s.renderer.svgFontCache[font] = resource + } + if !s.seen[resource.Name] { + s.seen[resource.Name] = true + s.fonts = append(s.fonts, resource) + fmt.Fprintf(&s.styles, ".%s{font-family:%s!important}", resource.Name, resource.Name) + } + s.SetClass(resource.Name) + s.SVG.RenderText(text, m) + s.SetClass() +}