diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 913845e..09239cf 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -1,5 +1,4 @@ const MM_TO_PX = 96 / 25.4; -const LOCAL_FONT_LOAD_LIMIT = 16; const FONT_DATABASE = "ofdgo"; const fontChannel = typeof BroadcastChannel === "function" ? new BroadcastChannel(FONT_DATABASE) : null; const COMPACT_LAYOUT = window.matchMedia("(max-width: 900px)"); @@ -765,7 +764,7 @@ async function queryLocalFonts() { } async function autoLoadDocumentLocalFonts(openSeq) { - if (!externalDocumentFontNames().length || !canReadLocalFonts() || state.systemFontPermission === "denied") { + if (!state.doc?.fonts?.some((font) => !font.embedded) || !canReadLocalFonts() || state.systemFontPermission === "denied") { return false; } setProgress("正在匹配字体", 62, STATUS.fonts); @@ -787,7 +786,6 @@ async function autoLoadDocumentLocalFonts(openSeq) { async function loadDocumentLocalFonts(available, openSeq = state.openSeq) { const docFonts = state.doc?.fonts || []; - const docNames = externalDocumentFontNames(); if (!docFonts.length) { state.localFonts = []; setStatus("暂无字体"); @@ -795,18 +793,14 @@ async function loadDocumentLocalFonts(available, openSeq = state.openSeq) { renderFontList(); return false; } - if (!docNames.length) { + if (docFonts.every((font) => font.embedded)) { state.localFonts = []; setStatus("文档字体均为内嵌"); updateFontSummary(); renderFontList(); return false; } - const docLoadLimit = Math.min(LOCAL_FONT_LOAD_LIMIT, Math.max(4, docNames.length * 3)); - let selected = await selectLocalFonts(available, docNames, docLoadLimit); - if (!selected.length) { - selected = await selectLocalFonts(available, [], 6); - } + const selected = await selectLocalFonts(available); if (openSeq !== state.openSeq) { return false; } @@ -834,32 +828,22 @@ async function loadDocumentLocalFonts(available, openSeq = state.openSeq) { return fonts.length > 0; } -function uniqueLocalFonts(fonts) { - const seen = new Set(); - const selected = []; - for (const font of fonts) { - const key = normalizeFontName(localFontName(font)); - if (!key || seen.has(key)) { - continue; - } - seen.add(key); - selected.push(font); - } - return selected; -} - -async function selectLocalFonts(fonts, names, limit) { +async function selectLocalFonts(fonts) { const available = new Map(); - for (const font of uniqueLocalFonts(fonts)) { - available.set(localFontName(font), font); - for (const name of [font.postscriptName, [font.family, font.style].filter(Boolean).join(" ")]) { - if (name) { - available.set(`${name}.ttf`, font); + for (const font of fonts) { + const names = [ + localFontName(font), + font.postscriptName && `${font.postscriptName}.ttf`, + font.family && `${[font.family, font.style].filter(Boolean).join(" ")}.ttf`, + ]; + for (const name of names) { + if (name && !available.has(name)) { + available.set(name, font); } } } - const matched = await callWASM("ofdgoFontFileMatches", [...available.keys()], names); - return uniqueLocalFonts(matched.map((name) => available.get(name))).slice(0, limit); + const matched = await callWASM("ofdgoMatchFontFiles", [...available.keys()]); + return [...new Set(matched.map((name) => available.get(name)))]; } async function fontData(fonts) { @@ -944,29 +928,11 @@ function fontRecords() { return [...state.localFonts, ...state.userFonts]; } -function externalDocumentFontNames() { - const names = []; - for (const font of state.doc?.fonts || []) { - if (font.embedded || font.status === "embedded") { - continue; - } - names.push(font.fontName, font.familyName); - } - return names; -} - function localFontName(font) { const name = font.fullName || font.family || font.postscriptName || "local-font"; return `${name}.ttf`; } -function normalizeFontName(name = "") { - return String(name) - .toLowerCase() - .replace(/\.[^.]+$/, "") - .replace(/[\s_\-()()]/g, ""); -} - async function applyFontChange(options = {}) { updateFontSummary(); renderFontList(); diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go index c7efeac..08bff3b 100644 --- a/internal/webui/wasm.go +++ b/internal/webui/wasm.go @@ -20,8 +20,6 @@ import ( "encoding/json" "fmt" "syscall/js" - - "github.com/xiaoqidun/ofdgo" ) // currentSession 当前WebUI文档会话 @@ -43,7 +41,7 @@ func RunWASM() { registerCallback("ofdgoExportFormats", exportFormats) registerCallback("ofdgoExportPage", exportPage) registerCallback("ofdgoExportPDF", exportPDF) - registerCallback("ofdgoFontFileMatches", fontFileMatches) + registerCallback("ofdgoMatchFontFiles", matchFontFiles) select {} } @@ -224,13 +222,14 @@ func bytesToJS(data []byte) js.Value { return value } -// fontFileMatches 匹配浏览器字体文件名称 +// matchFontFiles 匹配文档所需的字体文件 // 入参: args 浏览器参数 // 返回: any 匹配的字体文件名称, error 错误信息 -func fontFileMatches(args []js.Value) (any, error) { - files := stringsFromJS(jsArg(args, 0)) - names := stringsFromJS(jsArg(args, 1)) - return ofdgo.FontFileMatches(files, false, false, names...), nil +func matchFontFiles(args []js.Value) (any, error) { + if currentSession == nil { + return nil, fmt.Errorf("ofd document is not opened") + } + return currentSession.Reader.MatchFontFiles(stringsFromJS(args[0])) } // bytesFromJS 从浏览器值读取二进制数据 @@ -279,31 +278,11 @@ func fontsFromJS(value js.Value) ([]FontFile, error) { // 入参: value 浏览器值 // 返回: []string 字符串列表 func stringsFromJS(value js.Value) []string { - if value.IsUndefined() || value.IsNull() { - return nil + items := make([]string, value.Length()) + for i := range items { + items[i] = value.Index(i).String() } - if js.Global().Get("Array").Call("isArray", value).Bool() { - length := value.Get("length").Int() - items := make([]string, 0, length) - for i := 0; i < length; i++ { - item := value.Index(i) - if !item.IsUndefined() && !item.IsNull() { - items = append(items, item.String()) - } - } - return items - } - return []string{value.String()} -} - -// jsArg 获取浏览器参数 -// 入参: args 浏览器参数列表, index 参数索引 -// 返回: js.Value 浏览器值 -func jsArg(args []js.Value, index int) js.Value { - if index < 0 || index >= len(args) { - return js.Undefined() - } - return args[index] + return items } // encodeResult 编码浏览器接口返回结果 diff --git a/ofdgo_font_fs.go b/ofdgo_font_fs.go index f5a6803..bb848d7 100644 --- a/ofdgo_font_fs.go +++ b/ofdgo_font_fs.go @@ -147,6 +147,36 @@ func FontFileMatches(files []string, bold, italic bool, names ...string) []strin return fontFileMatchNames(fontFileMatches(fontFileCandidates(files, path.Base), names, bold, italic)) } +// MatchFontFiles 按文档字体名称和样式选择首选文件,跳过内嵌字体 +// 入参: files 可用字体文件名 +// 返回: []string 所需字体文件名, error 错误信息 +func (r *Reader) MatchFontFiles(files []string) ([]string, error) { + fonts, err := r.Fonts() + if err != nil { + return nil, err + } + candidates := fontFileCandidates(files, path.Base) + names := make([]string, 0, len(fonts)) + seen := make(map[string]bool) + for _, font := range fonts { + if font.FontFile != "" { + continue + } + matches := fontFileMatches(candidates, []string{font.FontName, font.FamilyName}, font.Bold, font.Italic) + if len(matches) == 0 { + matches = fontFileMatches(candidates, nil, font.Bold, font.Italic) + } + if len(matches) > 0 { + name := matches[0].name + if !seen[name] { + names = append(names, name) + seen[name] = true + } + } + } + return names, nil +} + // matchStyle 匹配指定样式的字体名称 // 入参: names 字体名称列表, bold 是否粗体, italic 是否斜体 // 返回: []fontFileMatch 字体匹配列表 diff --git a/ofdgo_font_info.go b/ofdgo_font_info.go index f0c5f0b..2f40f4d 100644 --- a/ofdgo_font_info.go +++ b/ofdgo_font_info.go @@ -49,9 +49,19 @@ type FontInfo struct { // Fonts 获取OFD声明的字体列表 // 返回: []Font 字体列表, error 错误信息 func (r *Reader) Fonts() ([]Font, error) { - if _, err := r.Doc(); err != nil { + doc, err := r.Doc() + if err != nil { return nil, err } + if !r.fontResourcesRead { + for _, page := range doc.Pages.Page { + r.loadPageResources(page) + } + for _, template := range doc.CommonData.TemplatePage { + r.loadPageResources(Page{BaseLoc: template.BaseLoc}) + } + r.fontResourcesRead = true + } fonts := make([]Font, 0, len(r.fontCache)) for _, font := range r.fontCache { if font != nil { diff --git a/ofdgo_reader.go b/ofdgo_reader.go index 28fa547..58d4c65 100644 --- a/ofdgo_reader.go +++ b/ofdgo_reader.go @@ -32,9 +32,11 @@ type Reader struct { RootDir string ResMap map[string]string fontCache map[string]*Font + fontResourcesRead bool colorSpaceCache map[string]*ColorSpace drawParamCache map[string]*DrawParam compositeGraphicUnitCache map[string]*CompositeGraphicUnit + pageHeaderCache map[string]PageContent doc *Document Stamps map[string][]Stamp Annots map[string][]Annotation @@ -78,6 +80,7 @@ func (r *Reader) initRoot() error { r.colorSpaceCache = make(map[string]*ColorSpace) r.drawParamCache = make(map[string]*DrawParam) r.compositeGraphicUnitCache = make(map[string]*CompositeGraphicUnit) + r.pageHeaderCache = make(map[string]PageContent) return nil } @@ -272,21 +275,38 @@ func (r *Reader) PageArea(page Page) (PageArea, error) { return r.resolvePageArea(content.Area, content.Template) } -// readPageHeader 读取页面区域和模板引用 +// loadPageResources 加载页面资源,不解析图元 +// 入参: page 页面引用 +func (r *Reader) loadPageResources(page Page) { + header, err := r.readPageHeader(page) + if err != nil { + return + } + for _, res := range header.PageRes { + r.loadRes(resolveResourcePath(page.BaseLoc, "", res)) + } +} + +// readPageHeader 读取页面区域、模板引用和资源声明 // 入参: page 页面对象 // 返回: PageContent 页面头部, error 错误信息 func (r *Reader) readPageHeader(page Page) (PageContent, error) { - f, err := r.openFile(r.ResPath(page.BaseLoc)) + name := r.ResPath(page.BaseLoc) + if content, ok := r.pageHeaderCache[name]; ok { + return content, nil + } + f, err := r.openFile(name) if err != nil { return PageContent{}, err } defer f.Close() var content PageContent d := xml.NewDecoder(f) +scan: for { token, err := d.Token() if err == io.EOF { - return content, nil + break } if err != nil { return PageContent{}, err @@ -302,10 +322,17 @@ func (r *Reader) readPageHeader(page Page) (PageContent, error) { } content.Template = append(content.Template, template) case "Area": - err := d.DecodeElement(&content.Area, &start) - return content, err + if err := d.DecodeElement(&content.Area, &start); err != nil { + return PageContent{}, err + } + case "PageRes": + var res string + if err := d.DecodeElement(&res, &start); err != nil { + return PageContent{}, err + } + content.PageRes = append(content.PageRes, res) case "Content": - return content, nil + break scan default: if err := d.Skip(); err != nil { return PageContent{}, err @@ -313,6 +340,8 @@ func (r *Reader) readPageHeader(page Page) (PageContent, error) { } } } + r.pageHeaderCache[name] = content + return content, nil } // resolvePageArea 解析页面区域的继承关系