diff --git a/public/packages/onlyoffice/9.4.0-develop/sdkjs/common/AllFonts.js b/public/packages/onlyoffice/9.4.0-develop/sdkjs/common/AllFonts.js index 8a42ffd2..48954425 100644 --- a/public/packages/onlyoffice/9.4.0-develop/sdkjs/common/AllFonts.js +++ b/public/packages/onlyoffice/9.4.0-develop/sdkjs/common/AllFonts.js @@ -455,6 +455,128 @@ window["__custom_font_registry__"] = { }; })(); +// 字体 catalog 属于 iframe 运行时而非单个文档。Word 在打开/刷新文档时会 +// 再次广播同一份 fonts:load;ComboBoxFonts 使用新的内部 id 合并,因此会把 +// 整套字体追加第二次。让同一 iframe 只消费首个完整 catalog,文档切换继续 +// 复用既有字体/缩略图缓存。 +(function (window) { + var state = + window.__ONLYOFFICE_FONT_CATALOG_STATE__ || + (window.__ONLYOFFICE_FONT_CATALOG_STATE__ = { delivered: false }); + function install(notification) { + if (!notification || notification.__ONLYOFFICE_FONT_CATALOG_ONCE__) { + return; + } + var trigger = notification.trigger; + notification.trigger = function (eventName) { + if (eventName === "fonts:load" && arguments.length > 1) { + if (state.delivered) { + return this; + } + state.delivered = true; + } + return trigger.apply(this, arguments); + }; + notification.__ONLYOFFICE_FONT_CATALOG_ONCE__ = true; + } + + function observeCommon(common) { + if (!common) { + return; + } + if (common.NotificationCenter) { + install(common.NotificationCenter); + return; + } + // AllFonts 先于 web-apps 加载;拦截 NotificationCenter 的首次赋值,确保 + // 其同步首轮 fonts:load 也经过上面的全局去重状态。 + var descriptor = Object.getOwnPropertyDescriptor(common, "NotificationCenter"); + if (descriptor && !descriptor.configurable) { + return; + } + var value; + Object.defineProperty(common, "NotificationCenter", { + configurable: true, + enumerable: true, + get: function () { + return value; + }, + set: function (next) { + value = next; + install(next); + }, + }); + } + + function reconcileFontComboStore() { + var toolbar = + window.DE && + typeof window.DE.getController === "function" && + window.DE.getController("Toolbar"); + var view = toolbar && typeof toolbar.getView === "function" && toolbar.getView("Toolbar"); + var combo = view && view.cmbFontName; + var store = combo && combo.store; + if (!store || !Array.isArray(store.models)) { + return; + } + + var seen = {}; + var models = []; + var duplicated = false; + for (var index = 0; index < store.models.length; index++) { + var model = store.models[index]; + var type = model && typeof model.get === "function" ? model.get("type") : undefined; + var name = model && typeof model.get === "function" ? model.get("name") : ""; + // type=1 是完整字体 catalog;type=4 是 OnlyOffice 的最近字体,保留它。 + if (type === 1 && name) { + if (seen[name]) { + duplicated = true; + continue; + } + seen[name] = true; + } + models.push(model); + } + if (!duplicated) { + return; + } + + store.reset(models); + combo.tiles = []; + if (combo.scroller) { + combo.scroller.destroy(); + delete combo.scroller; + } + combo._scrollerIsInited = false; + combo.rendered = false; + combo.render(window.$(combo.el)); + combo._fontsArray = store.toJSON(); + } + + // 仅在 SDK 真正追加重复 catalog 后重建;正常打开/滚动没有额外工作。 + window.setInterval(reconcileFontComboStore, 50); + + if (window.Common) { + observeCommon(window.Common); + return; + } + + var commonDescriptor = Object.getOwnPropertyDescriptor(window, "Common"); + if (!commonDescriptor || commonDescriptor.configurable) { + var commonValue; + Object.defineProperty(window, "Common", { + configurable: true, + get: function () { + return commonValue; + }, + set: function (next) { + commonValue = next; + observeCommon(next); + }, + }); + } +})(window); + // 9.4 已直接从上面的 __fonts_files / __fonts_infos 构建 XDc / Vbb。 // web-apps 比 SDK 晚加载,单独刷新 ComboBox 的搜索缓存即可;不改写字体 // 加载器、二进制流或文档排版管线,避免影响 OnlyOffice 自带字体渲染。 @@ -521,13 +643,22 @@ window["__custom_font_registry__"] = { return; } if (!store || typeof store.each !== "function") return; + var emptyModels = []; store.each(function (model) { if (!model || typeof model.get !== "function") return; + var name = model.get("name"); + if (typeof name !== "string" || !name.trim()) { + emptyModels.push(model); + return; + } var idx = model.get("imgidx"); if (typeof idx !== "number" || !isFinite(idx) || idx < 0 || idx > 512) { model.set("imgidx", 0, { silent: true }); } }); + emptyModels.forEach(function (model) { + store.remove(model); + }); } function forceSafeSpriteIndexes(store) { @@ -543,13 +674,149 @@ window["__custom_font_registry__"] = { }); } + function isRegisteredCustomFontName(name) { + for (var id in registry) { + if (!Object.prototype.hasOwnProperty.call(registry, id)) continue; + var names = Array.isArray(registry[id]) ? registry[id] : []; + if (names.indexOf(name) >= 0) return true; + } + return false; + } + function patchSpriteThumbDecoder(combo) { var sprite = combo && combo.spriteThumbs; - if (!sprite || typeof sprite.getImage !== "function" || sprite.__CUSTOM_FONT_SPRITE_GUARD__) { + if ( + !sprite || + typeof sprite.getImage !== "function" || + sprite.getImage.__CUSTOM_FONT_DYNAMIC_TILE_RENDERER__ + ) { return; } var origGetImage = sprite.getImage; + var previewFonts = {}; + var fontXorKey = [160, 102, 214, 32, 20, 150, 71, 250, 149, 105, 184, 80, 176, 65, 73, 72]; + + function isCustomFontName(name) { + for (var id in registry) { + if (!Object.prototype.hasOwnProperty.call(registry, id)) continue; + var names = Array.isArray(registry[id]) ? registry[id] : []; + if (names.indexOf(name) >= 0) return true; + } + return false; + } + + function getCustomFontId(name) { + for (var id in registry) { + if (!Object.prototype.hasOwnProperty.call(registry, id)) continue; + var names = Array.isArray(registry[id]) ? registry[id] : []; + if (names.indexOf(name) >= 0) return id; + } + return null; + } + + function refreshVisibleTiles(combo) { + window.setTimeout(function () { + if (!combo || (typeof combo.isMenuOpen === "function" && !combo.isMenuOpen())) { + return; + } + combo.flushVisibleFontsTiles(); + combo.updateVisibleFontsTiles(); + }, 0); + } + + function loadPreviewFont(id, combo) { + if (!id || !window.FontFace || !window.fetch) return null; + var state = previewFonts[id]; + if (state) return state; + + state = { + family: "OnlyOfficeCustomPreview_" + id, + loaded: false, + }; + previewFonts[id] = state; + state.promise = window + .fetch(new URL("../../../../fonts/" + id, window.location.href).href) + .then(function (response) { + if (!response.ok) throw new Error("Failed to load custom font preview"); + return response.arrayBuffer(); + }) + .then(function (buffer) { + var bytes = new Uint8Array(buffer); + for (var index = 0; index < Math.min(32, bytes.length); index++) { + bytes[index] ^= fontXorKey[index % fontXorKey.length]; + } + return new FontFace(state.family, bytes.buffer).load(); + }) + .then(function (face) { + document.fonts.add(face); + state.loaded = true; + refreshVisibleTiles(combo); + }) + .catch(function () { + state.failed = true; + }); + return state; + } + + function getCustomFontName(index) { + var catalog = window.AscFonts && window.AscFonts.Vbb; + if (!Array.isArray(catalog)) return null; + for (var i = 0; i < catalog.length; i++) { + var entry = catalog[i]; + if (entry && entry.Vri === index && isCustomFontName(entry.xa)) { + return entry.xa; + } + } + return null; + } + + function createCustomFontThumbnail(name) { + // PPT 首次展开字体菜单时 sprite 图片可能仍在解码,width/height 都是 + // 0。使用 OnlyOffice tile 的固定 CSS 尺寸,避免生成 0×0 canvas。 + var tileWidth = sprite.width || 300; + var tileHeight = sprite.heightOne || sprite.height || 28; + var fontId = getCustomFontId(name); + var preview = loadPreviewFont(fontId, combo); + // ComboBoxFonts 的虚拟列表会在关闭时移除返回的 canvas。必须为每个 + // getImage() 调用创建新节点,不能缓存或复用,否则第二次打开时 + // flushVisibleFontsTiles 会对 parentNode === null 的旧节点 removeChild。 + var canvas = document.createElement("canvas"); + canvas.width = tileWidth; + canvas.height = tileHeight; + // 原生 getImage() 的 canvas 使用设备像素作为 width/height,但在 CSS + // 中固定为 300×28。保持同一尺寸契约,避免高 DPR 下占满菜单行或 + // 让 hover 区域与内置字体不一致。 + var scale = tileWidth / 300 || 1; + canvas.style.width = canvas.width / scale + "px"; + canvas.style.height = canvas.height / scale + "px"; + canvas.style.pointerEvents = "none"; + var context = canvas.getContext("2d"); + if (!context) return canvas; + + // 保持透明底色,font-item 的原生 hover 背景才能透出。 + context.clearRect(0, 0, canvas.width, canvas.height); + context.fillStyle = "#000000"; + context.textBaseline = "middle"; + var size = Math.floor(canvas.height * 0.64); + do { + context.font = size + "px " + (preview && preview.loaded ? preview.family : "Arial"); + if (context.measureText(name).width <= canvas.width) break; + size -= 1; + } while (size > 8); + // 与原生 fonts_thumbnail 保持 18 CSS px 左侧留白。 + context.fillText(name, Math.round(18 * scale), canvas.height / 2); + return canvas; + } + sprite.getImage = function (index) { + // Vri 是 SDK catalog 分配给自定义字体的缩略图索引。静态 sprite + // 没有这些 tile,原实现会把越界索引钳制到 0(Abyssinica SIL)。 + // 在原生 sprite API 层补齐对应 canvas,保留虚拟列表的节点和流程。 + var customFontName = getCustomFontName(index); + if (customFontName) { + sprite.__CUSTOM_FONT_DYNAMIC_TILE_NAMES__[index] = customFontName; + return createCustomFontThumbnail(customFontName); + } // updateVisibleFontsTiles 会将 imgidx/r 传到这里。部分自定义字体的 // imgidx 实为字节偏移(33600 / 134400),且可在 store 清理后的 // 虚拟滚动回调中才出现;在最终解码入口钳制才是可靠边界。 @@ -570,7 +837,14 @@ window["__custom_font_registry__"] = { args[0] = index; return origGetImage.apply(this, args); }; + // Excel / PPT 会在 ComboBox 构造的后半段替换实例 getImage();不能只用 + // sprite 级布尔值判断是否已 patch。标记具体函数,若 SDK 覆盖它,下一次 + // updateVisibleFontsTiles 会以新的原生实现为基准重新包裹。 + sprite.getImage.__CUSTOM_FONT_DYNAMIC_TILE_RENDERER__ = true; sprite.__CUSTOM_FONT_SPRITE_GUARD__ = true; + sprite.__CUSTOM_FONT_DYNAMIC_TILES__ = true; + sprite.__CUSTOM_FONT_DYNAMIC_TILE_NAMES__ = {}; + sprite.__CUSTOM_FONT_CREATE_TILE__ = createCustomFontThumbnail; } if (typeof proto.selectCandidate === "function") { @@ -631,12 +905,48 @@ window["__custom_font_registry__"] = { if (typeof proto.updateVisibleFontsTiles === "function") { var origUpdateVisibleFontsTiles = proto.updateVisibleFontsTiles; + + function syncRegisteredCustomFontTiles(combo) { + if (!combo || !combo.el || !combo.store || !combo.spriteThumbs || !Array.isArray(combo.tiles)) { + return; + } + var createTile = combo.spriteThumbs.__CUSTOM_FONT_CREATE_TILE__; + if (typeof createTile !== "function") { + return; + } + combo.store.each(function (model, index) { + var name = model && model.get && model.get("name"); + if (!isRegisteredCustomFontName(name)) { + return; + } + var id = model.get("id"); + var item = combo.el.querySelector("li[id='" + id + "']"); + var anchor = item && item.querySelector("a.font-item"); + if (!anchor) { + return; + } + var oldTile = combo.tiles[index]; + if (oldTile && oldTile.parentNode) { + oldTile.parentNode.removeChild(oldTile); + } + var extraTiles = anchor.querySelectorAll("canvas"); + for (var tileIndex = extraTiles.length - 1; tileIndex >= 0; tileIndex--) { + extraTiles[tileIndex].parentNode.removeChild(extraTiles[tileIndex]); + } + var tile = createTile(name); + combo.tiles[index] = tile; + anchor.appendChild(tile); + }); + } + proto.updateVisibleFontsTiles = function () { sanitizeStore(this.store); forceSafeSpriteIndexes(this.store); patchSpriteThumbDecoder(this); try { - return origUpdateVisibleFontsTiles.apply(this, arguments); + var result = origUpdateVisibleFontsTiles.apply(this, arguments); + syncRegisteredCustomFontTiles(this); + return result; } catch (err) { // 个别 DOCX 的字体列表会在菜单打开时由 SDK 异步补项;若该补项 // 恰好携带 font file offset,第一次渲染仍可能命中旧值。清理后仅 @@ -646,7 +956,9 @@ window["__custom_font_registry__"] = { /Invalid typed array length/.test(String(err && err.message)) ) { forceSafeSpriteIndexes(this.store); - return origUpdateVisibleFontsTiles.apply(this, arguments); + var retryResult = origUpdateVisibleFontsTiles.apply(this, arguments); + syncRegisteredCustomFontTiles(this); + return retryResult; } throw err; } diff --git a/scripts/assets/onlyoffice/custom-fonts/AllFonts.custom-font.patch.js b/scripts/assets/onlyoffice/custom-fonts/AllFonts.custom-font.patch.js index b029249a..dc3cb247 100644 --- a/scripts/assets/onlyoffice/custom-fonts/AllFonts.custom-font.patch.js +++ b/scripts/assets/onlyoffice/custom-fonts/AllFonts.custom-font.patch.js @@ -80,6 +80,128 @@ window["__custom_font_registry__"] = { }; })(); +// 字体 catalog 属于 iframe 运行时而非单个文档。Word 在打开/刷新文档时会 +// 再次广播同一份 fonts:load;ComboBoxFonts 使用新的内部 id 合并,因此会把 +// 整套字体追加第二次。让同一 iframe 只消费首个完整 catalog,文档切换继续 +// 复用既有字体/缩略图缓存。 +(function (window) { + var state = + window.__ONLYOFFICE_FONT_CATALOG_STATE__ || + (window.__ONLYOFFICE_FONT_CATALOG_STATE__ = { delivered: false }); + function install(notification) { + if (!notification || notification.__ONLYOFFICE_FONT_CATALOG_ONCE__) { + return; + } + var trigger = notification.trigger; + notification.trigger = function (eventName) { + if (eventName === "fonts:load" && arguments.length > 1) { + if (state.delivered) { + return this; + } + state.delivered = true; + } + return trigger.apply(this, arguments); + }; + notification.__ONLYOFFICE_FONT_CATALOG_ONCE__ = true; + } + + function observeCommon(common) { + if (!common) { + return; + } + if (common.NotificationCenter) { + install(common.NotificationCenter); + return; + } + // AllFonts 先于 web-apps 加载;拦截 NotificationCenter 的首次赋值,确保 + // 其同步首轮 fonts:load 也经过上面的全局去重状态。 + var descriptor = Object.getOwnPropertyDescriptor(common, "NotificationCenter"); + if (descriptor && !descriptor.configurable) { + return; + } + var value; + Object.defineProperty(common, "NotificationCenter", { + configurable: true, + enumerable: true, + get: function () { + return value; + }, + set: function (next) { + value = next; + install(next); + }, + }); + } + + function reconcileFontComboStore() { + var toolbar = + window.DE && + typeof window.DE.getController === "function" && + window.DE.getController("Toolbar"); + var view = toolbar && typeof toolbar.getView === "function" && toolbar.getView("Toolbar"); + var combo = view && view.cmbFontName; + var store = combo && combo.store; + if (!store || !Array.isArray(store.models)) { + return; + } + + var seen = {}; + var models = []; + var duplicated = false; + for (var index = 0; index < store.models.length; index++) { + var model = store.models[index]; + var type = model && typeof model.get === "function" ? model.get("type") : undefined; + var name = model && typeof model.get === "function" ? model.get("name") : ""; + // type=1 是完整字体 catalog;type=4 是 OnlyOffice 的最近字体,保留它。 + if (type === 1 && name) { + if (seen[name]) { + duplicated = true; + continue; + } + seen[name] = true; + } + models.push(model); + } + if (!duplicated) { + return; + } + + store.reset(models); + combo.tiles = []; + if (combo.scroller) { + combo.scroller.destroy(); + delete combo.scroller; + } + combo._scrollerIsInited = false; + combo.rendered = false; + combo.render(window.$(combo.el)); + combo._fontsArray = store.toJSON(); + } + + // 仅在 SDK 真正追加重复 catalog 后重建;正常打开/滚动没有额外工作。 + window.setInterval(reconcileFontComboStore, 50); + + if (window.Common) { + observeCommon(window.Common); + return; + } + + var commonDescriptor = Object.getOwnPropertyDescriptor(window, "Common"); + if (!commonDescriptor || commonDescriptor.configurable) { + var commonValue; + Object.defineProperty(window, "Common", { + configurable: true, + get: function () { + return commonValue; + }, + set: function (next) { + commonValue = next; + observeCommon(next); + }, + }); + } +})(window); + // 9.4 已直接从上面的 __fonts_files / __fonts_infos 构建 XDc / Vbb。 // web-apps 比 SDK 晚加载,单独刷新 ComboBox 的搜索缓存即可;不改写字体 // 加载器、二进制流或文档排版管线,避免影响 OnlyOffice 自带字体渲染。 @@ -138,6 +260,13 @@ window["__custom_font_registry__"] = { return !!proto; } + function hasVisibleFontName(name) { + return ( + typeof name === "string" && + /[^\s\u200B-\u200D\u2060\uFEFF]/.test(name) + ); + } + function sanitizeStore(store) { // 完整补丁安装后优先走它的 sprite 边界检测;早期初始化时则使用 // 保守兜底,避免 font file 的字节偏移被当成缩略图索引。 @@ -146,13 +275,30 @@ window["__custom_font_registry__"] = { return; } if (!store || typeof store.each !== "function") return; + var emptyModels = []; + var normalFontNames = {}; store.each(function (model) { if (!model || typeof model.get !== "function") return; + var name = model.get("name"); + if (!hasVisibleFontName(name)) { + emptyModels.push(model); + return; + } + if (model.get("type") === 1) { + if (normalFontNames[name]) { + emptyModels.push(model); + return; + } + normalFontNames[name] = true; + } var idx = model.get("imgidx"); if (typeof idx !== "number" || !isFinite(idx) || idx < 0 || idx > 512) { model.set("imgidx", 0, { silent: true }); } }); + emptyModels.forEach(function (model) { + store.remove(model); + }); } function forceSafeSpriteIndexes(store) { @@ -168,13 +314,191 @@ window["__custom_font_registry__"] = { }); } + function isRegisteredCustomFontName(name) { + for (var id in registry) { + if (!Object.prototype.hasOwnProperty.call(registry, id)) continue; + var aliases = Array.isArray(registry[id]) ? registry[id] : []; + if (aliases.indexOf(name) >= 0) return true; + } + return false; + } + + function findCustomFontNameByThumbnailIndex(index, combo, sprite) { + if (typeof index !== "number" || !isFinite(index)) return null; + var name = null; + var store = combo && combo.store; + if (store && typeof store.each === "function") { + var scale = Math.floor((sprite && sprite.width) / 300) || 1; + store.each(function (model) { + if (name || !model || typeof model.get !== "function") return; + var candidate = model.get("name"); + if ( + Math.floor(model.get("imgidx") / scale) === index && + typeof candidate === "string" && + isRegisteredCustomFontName(candidate) + ) { + name = candidate; + } + }); + } + if (name) return name; + + // Word 的 Vbb、Excel 和 PPT 的 font catalog 字段不同。缩略图层只 + // 需要稳定的 registry 顺序:静态 sprite 后的连续 8 个 tile 对应 + // registry 中的 8 个别名,而不依赖某一编辑器专有的 catalog。 + var base = + sprite && typeof sprite.count === "number" && sprite.count > 0 + ? Math.floor(sprite.count) + : 144; + var offset = Math.floor(index) - base; + if (offset < 0) return null; + var names = []; + for (var id in registry) { + if (!Object.prototype.hasOwnProperty.call(registry, id)) continue; + var aliases = Array.isArray(registry[id]) ? registry[id] : []; + for (var i = 0; i < aliases.length; i++) { + if (aliases[i]) names.push(aliases[i]); + } + } + return names[offset] || null; + } + function patchSpriteThumbDecoder(combo) { var sprite = combo && combo.spriteThumbs; - if (!sprite || typeof sprite.getImage !== "function" || sprite.__CUSTOM_FONT_SPRITE_GUARD__) { + if ( + !sprite || + typeof sprite.getImage !== "function" || + sprite.getImage.__CUSTOM_FONT_DYNAMIC_TILE_RENDERER__ + ) { return; } var origGetImage = sprite.getImage; + var previewFonts = {}; + var fontXorKey = [160, 102, 214, 32, 20, 150, 71, 250, 149, 105, 184, 80, 176, 65, 73, 72]; + + function isCustomFontName(name) { + for (var id in registry) { + if (!Object.prototype.hasOwnProperty.call(registry, id)) continue; + var names = Array.isArray(registry[id]) ? registry[id] : []; + if (names.indexOf(name) >= 0) return true; + } + return false; + } + + function getCustomFontId(name) { + for (var id in registry) { + if (!Object.prototype.hasOwnProperty.call(registry, id)) continue; + var names = Array.isArray(registry[id]) ? registry[id] : []; + if (names.indexOf(name) >= 0) return id; + } + return null; + } + + function refreshVisibleTiles(combo) { + window.setTimeout(function () { + if (!combo || (typeof combo.isMenuOpen === "function" && !combo.isMenuOpen())) { + return; + } + combo.flushVisibleFontsTiles(); + combo.updateVisibleFontsTiles(); + }, 0); + } + + function loadPreviewFont(id, combo) { + if (!id || !window.FontFace || !window.fetch) return null; + var state = previewFonts[id]; + if (state) return state; + + state = { + family: "OnlyOfficeCustomPreview_" + id, + loaded: false, + }; + previewFonts[id] = state; + state.promise = window + .fetch(new URL("../../../../fonts/" + id, window.location.href).href) + .then(function (response) { + if (!response.ok) throw new Error("Failed to load custom font preview"); + return response.arrayBuffer(); + }) + .then(function (buffer) { + var bytes = new Uint8Array(buffer); + for (var index = 0; index < Math.min(32, bytes.length); index++) { + bytes[index] ^= fontXorKey[index % fontXorKey.length]; + } + return new FontFace(state.family, bytes.buffer).load(); + }) + .then(function (face) { + document.fonts.add(face); + state.loaded = true; + refreshVisibleTiles(combo); + }) + .catch(function () { + state.failed = true; + }); + return state; + } + + function getCustomFontName(index) { + var name = findCustomFontNameByThumbnailIndex(index, combo, sprite); + if (name) return name; + var catalog = window.AscFonts && window.AscFonts.Vbb; + if (!Array.isArray(catalog)) return null; + for (var i = 0; i < catalog.length; i++) { + var entry = catalog[i]; + if (entry && entry.Vri === index && isCustomFontName(entry.xa)) { + return entry.xa; + } + } + return null; + } + + function createCustomFontThumbnail(name) { + // PPT 首次展开字体菜单时 sprite 图片可能仍在解码,width/height 都是 + // 0。使用 OnlyOffice tile 的固定 CSS 尺寸,避免生成 0×0 canvas。 + var tileWidth = sprite.width || 300; + var tileHeight = sprite.heightOne || sprite.height || 28; + var fontId = getCustomFontId(name); + var preview = loadPreviewFont(fontId, combo); + // ComboBoxFonts 的虚拟列表会在关闭时移除返回的 canvas。必须为每个 + // getImage() 调用创建新节点,不能缓存或复用,否则第二次打开时 + // flushVisibleFontsTiles 会对 parentNode === null 的旧节点 removeChild。 + var canvas = document.createElement("canvas"); + canvas.width = tileWidth; + canvas.height = tileHeight; + // 原生 getImage() 的 canvas 使用设备像素作为 width/height,但在 CSS + // 中固定为 300×28。保持同一尺寸契约,避免高 DPR 下占满菜单行或 + // 让 hover 区域与内置字体不一致。 + var scale = tileWidth / 300 || 1; + canvas.style.width = canvas.width / scale + "px"; + canvas.style.height = canvas.height / scale + "px"; + canvas.style.pointerEvents = "none"; + var context = canvas.getContext("2d"); + if (!context) return canvas; + + // 保持透明底色,font-item 的原生 hover 背景才能透出。 + context.clearRect(0, 0, canvas.width, canvas.height); + context.fillStyle = "#000000"; + context.textBaseline = "middle"; + var size = Math.floor(canvas.height * 0.64); + do { + context.font = size + "px " + (preview && preview.loaded ? preview.family : "Arial"); + if (context.measureText(name).width <= canvas.width) break; + size -= 1; + } while (size > 8); + // 与原生 fonts_thumbnail 保持 18 CSS px 左侧留白。 + context.fillText(name, Math.round(18 * scale), canvas.height / 2); + return canvas; + } + sprite.getImage = function (index) { + // Vri 是 SDK catalog 分配给自定义字体的缩略图索引。静态 sprite + // 没有这些 tile,原实现会把越界索引钳制到 0(Abyssinica SIL)。 + // 在原生 sprite API 层补齐对应 canvas,保留虚拟列表的节点和流程。 + var customFontName = getCustomFontName(index); + if (customFontName) { + sprite.__CUSTOM_FONT_DYNAMIC_TILE_NAMES__[index] = customFontName; + return createCustomFontThumbnail(customFontName); + } // updateVisibleFontsTiles 会将 imgidx/r 传到这里。部分自定义字体的 // imgidx 实为字节偏移(33600 / 134400),且可在 store 清理后的 // 虚拟滚动回调中才出现;在最终解码入口钳制才是可靠边界。 @@ -195,7 +519,14 @@ window["__custom_font_registry__"] = { args[0] = index; return origGetImage.apply(this, args); }; + // Excel / PPT 会在 ComboBox 构造的后半段替换实例 getImage();不能只用 + // sprite 级布尔值判断是否已 patch。标记具体函数,若 SDK 覆盖它,下一次 + // updateVisibleFontsTiles 会以新的原生实现为基准重新包裹。 + sprite.getImage.__CUSTOM_FONT_DYNAMIC_TILE_RENDERER__ = true; sprite.__CUSTOM_FONT_SPRITE_GUARD__ = true; + sprite.__CUSTOM_FONT_DYNAMIC_TILES__ = true; + sprite.__CUSTOM_FONT_DYNAMIC_TILE_NAMES__ = {}; + sprite.__CUSTOM_FONT_CREATE_TILE__ = createCustomFontThumbnail; } if (typeof proto.selectCandidate === "function") { @@ -225,6 +556,52 @@ window["__custom_font_registry__"] = { } } + // 导入文档时,web-apps 会异步把 document font table 和全局 catalog + // 依次写入同一个 Backbone store。若仅在展开菜单时清理,两个同名项 + // 会在此间隔内留在列表里。监听 store 的新增事件,在当前事件栈完成 + // 后统一去重,避免干扰 OnlyOffice 的 store.set()/render() 过程。 + function installStoreSanitizer(combo) { + var store = combo && combo.store; + if ( + !store || + typeof store.on !== "function" || + combo.__CUSTOM_FONT_STORE_SANITIZER__ === store + ) { + return; + } + var scheduled = false; + var sanitize = function () { + if (scheduled) return; + scheduled = true; + window.setTimeout(function () { + scheduled = false; + sanitizeStore(store); + forceSafeSpriteIndexes(store); + // 文档加载阶段不能主动驱动 ComboBox 的 tile 刷新;Word 引擎仍在 + // 建立字体/段落状态时会因此进入空引用。菜单已展开时才刷新可见项。 + if ( + combo && + typeof combo.isMenuOpen === "function" && + combo.isMenuOpen() && + typeof combo.updateVisibleFontsTiles === "function" + ) { + combo.updateVisibleFontsTiles(); + } + }, 0); + }; + store.on("add", sanitize); + store.on("reset", sanitize); + combo.__CUSTOM_FONT_STORE_SANITIZER__ = store; + } + + if (typeof proto.fillFonts === "function") { + var origFillFonts = proto.fillFonts; + proto.fillFonts = function () { + installStoreSanitizer(this); + return origFillFonts.apply(this, arguments); + }; + } + // OnlyOffice 的字体候选搜索原本只在 keyup 中执行。系统粘贴没有普通 // 字符 keyup,导致“仿宋_GB2312”等别名在回车时回退为等线。 if (typeof proto.onInputKeyDown === "function") { @@ -256,12 +633,113 @@ window["__custom_font_registry__"] = { if (typeof proto.updateVisibleFontsTiles === "function") { var origUpdateVisibleFontsTiles = proto.updateVisibleFontsTiles; + + function normalizeFontItemDom(combo) { + if (!combo || !combo.el) { + return false; + } + var seen = {}; + var removed = false; + var listItems = combo.el.getElementsByTagName("li"); + // 使用静态快照,移除节点不会影响本次遍历索引。 + var items = Array.prototype.slice.call(listItems); + for (var index = 0; index < items.length; index++) { + var item = items[index]; + if (!item.id || !item.querySelector("a.font-item")) { + continue; + } + if (seen[item.id]) { + item.parentNode.removeChild(item); + removed = true; + continue; + } + seen[item.id] = true; + } + return removed; + } + + function resetFontTiles(combo) { + if (!combo || !combo.el) { + return; + } + var canvases = combo.el.querySelectorAll("a.font-item canvas"); + for (var index = 0; index < canvases.length; index++) { + var canvas = canvases[index]; + if (canvas.parentNode) { + canvas.parentNode.removeChild(canvas); + } + } + combo.tiles = []; + } + + function syncRegisteredCustomFontTiles(combo) { + if ( + !combo || + !combo.el || + !combo.store || + !combo.spriteThumbs || + !Array.isArray(combo.tiles) + ) { + return; + } + var scale = Math.floor(combo.spriteThumbs.width / 300) || 1; + combo.store.each(function (model, index) { + if (!model || !isRegisteredCustomFontName(model.get("name"))) { + return; + } + // recent 字体会由 onInsertItem 直接 prepend DOM;在某些导入路径 + // 下会留下重复的 li,故 anchors[index] 不再可靠。按 store model + // id 找到实际条目,避免把末尾自定义字体的 tile 附到错误行。 + var id = model.get("id"); + var listItems = combo.el.getElementsByTagName("li"); + var anchor = null; + for (var itemIndex = 0; itemIndex < listItems.length; itemIndex++) { + if (listItems[itemIndex].id !== id) { + continue; + } + anchor = listItems[itemIndex].querySelector("a.font-item"); + if (anchor) { + break; + } + } + if (!anchor) { + return; + } + var tile = combo.tiles[index]; + if (tile && tile.parentNode === anchor) { + return; + } + if (tile && tile.parentNode) { + tile.parentNode.removeChild(tile); + } + var createTile = combo.spriteThumbs.__CUSTOM_FONT_CREATE_TILE__; + if (typeof createTile === "function") { + tile = createTile(model.get("name")); + } else { + var spriteIndex = Math.floor(model.get("imgidx") / scale); + if (!isFinite(spriteIndex)) { + return; + } + tile = combo.spriteThumbs.getImage(spriteIndex); + } + combo.tiles[index] = tile; + anchor.appendChild(tile); + }); + } + proto.updateVisibleFontsTiles = function () { sanitizeStore(this.store); forceSafeSpriteIndexes(this.store); patchSpriteThumbDecoder(this); + if (normalizeFontItemDom(this)) { + // duplicated recent li 会让原生函数用 anchors[index] 取到错误行; + // 清空缓存后由它按修正后的 DOM 顺序重建当前可视 tile。 + resetFontTiles(this); + } try { - return origUpdateVisibleFontsTiles.apply(this, arguments); + var result = origUpdateVisibleFontsTiles.apply(this, arguments); + syncRegisteredCustomFontTiles(this); + return result; } catch (err) { // 个别 DOCX 的字体列表会在菜单打开时由 SDK 异步补项;若该补项 // 恰好携带 font file offset,第一次渲染仍可能命中旧值。清理后仅 @@ -271,7 +749,9 @@ window["__custom_font_registry__"] = { /Invalid typed array length/.test(String(err && err.message)) ) { forceSafeSpriteIndexes(this.store); - return origUpdateVisibleFontsTiles.apply(this, arguments); + var retryResult = origUpdateVisibleFontsTiles.apply(this, arguments); + syncRegisteredCustomFontTiles(this); + return retryResult; } throw err; } @@ -1151,6 +1631,31 @@ window["__custom_font_registry__"] = { return set; } + function getRegistryThumbnailIndex(name, ctx) { + if (typeof name !== "string" || !name) { + return null; + } + var registry = (ctx && ctx.registry) || loadCustomFontRegistry(); + var base = + ctx && typeof ctx.spriteCount === "number" && ctx.spriteCount > 0 + ? ctx.spriteCount + : 144; + var offset = 0; + for (var id in registry) { + if (!Object.prototype.hasOwnProperty.call(registry, id)) { + continue; + } + var names = registryNames(registry[id] || {}); + for (var i = 0; i < names.length; i++) { + if (names[i] === name) { + return base + offset; + } + offset++; + } + } + return null; + } + function fixFontPickerThumbnails(registry) { var catalog = window.AscFonts && window.AscFonts.$4a; if (!catalog) { @@ -1281,6 +1786,10 @@ window["__custom_font_registry__"] = { function resolveSafeThumbnailIndex(name, rawThumb, ctx) { ctx = ctx || getThumbnailSanitizeContext(); + var customIndex = getRegistryThumbnailIndex(name, ctx); + if (customIndex !== null) { + return customIndex; + } if (fontThumbnailNeedsFallback(name, rawThumb, ctx.nameSet, ctx.maxKnown, ctx.spriteCount)) { return ctx.fallback; } @@ -1290,22 +1799,49 @@ window["__custom_font_registry__"] = { return Math.floor(rawThumb); } + function hasVisibleFontName(name) { + return ( + typeof name === "string" && + /[^\s\u200B-\u200D\u2060\uFEFF]/.test(name) + ); + } + function sanitizeFontComboStore(store) { if (!store || typeof store.each !== "function") { return; } var ctx = getThumbnailSanitizeContext(); + var emptyModels = []; + var normalFontNames = {}; store.each(function (model) { if (!model || typeof model.get !== "function") { return; } var name = model.get("name"); + // OnlyOffice 会把没有可用名称的字体也放进 ComboBox store;它们没有 + // 可渲染的预览内容,却仍占用一行,表现为菜单底部的空白字体项。 + if (!hasVisibleFontName(name)) { + emptyModels.push(model); + return; + } + // 导入文档的 font table 与全局 catalog 都可能推送完整列表。按名称 + // 合并普通字体(type === 1),保留顶部的「最近使用」记录。 + if (model.get("type") === 1) { + if (normalFontNames[name]) { + emptyModels.push(model); + return; + } + normalFontNames[name] = true; + } var imgidx = model.get("imgidx"); var safe = resolveSafeThumbnailIndex(name, imgidx, ctx); if (safe !== imgidx) { model.set("imgidx", safe); } }); + emptyModels.forEach(function (model) { + store.remove(model); + }); } // 首段 registry 补丁与本段完整编辑器补丁处于不同 IIFE 作用域;暴露一个 @@ -1340,26 +1876,40 @@ window["__custom_font_registry__"] = { } var ctx = getThumbnailSanitizeContext(); if (typeof collection.each === "function") { + var emptyModels = []; collection.each(function (model) { if (!model || typeof model.get !== "function") { return; } var name = model.get("name"); + if (!hasVisibleFontName(name)) { + emptyModels.push(model); + return; + } var imgidx = model.get("imgidx"); var safe = resolveSafeThumbnailIndex(name, imgidx, ctx); if (safe !== imgidx) { model.set("imgidx", safe); } }); + if (typeof collection.remove === "function") { + emptyModels.forEach(function (model) { + collection.remove(model); + }); + } return; } if (Array.isArray(collection)) { - for (var i = 0; i < collection.length; i++) { + for (var i = collection.length - 1; i >= 0; i--) { var row = collection[i]; if (!row) { continue; } var rowName = row.name; + if (!hasVisibleFontName(rowName)) { + collection.splice(i, 1); + continue; + } var rowIdx = row.imgidx; var rowSafe = resolveSafeThumbnailIndex(rowName, rowIdx, ctx); if (rowSafe !== rowIdx) { @@ -3407,5 +3957,10 @@ window["__custom_font_registry__"] = { }; }; + // 首段兼容补丁仅覆盖当时已加载的 ComboBoxFonts;三种编辑器的完整 + // catalog 与 web-apps 往往更晚初始化,必须分别启动这些安装轮询。 + tryInstallAll(); + waitForWebAppsComboFontHook(); + waitForRuntimeCustomFontPicker(); pollUntilReady(); })(window); diff --git a/src/components/onlyoffice-web-comp/const/index.ts b/src/components/onlyoffice-web-comp/const/index.ts index aaeeb361..0b6a266b 100644 --- a/src/components/onlyoffice-web-comp/const/index.ts +++ b/src/components/onlyoffice-web-comp/const/index.ts @@ -115,7 +115,7 @@ export type StaticResource = { }; export type OnlyOfficeStaticResourceOptions = { - /** CDN packages 根地址,例如 https://8057ac0e.onlyoffice-packages.pages.dev。 */ + /** CDN packages 根地址,例如 https://b26830b7.onlyoffice-packages.pages.dev。 */ cdnOrigin?: string | null; /** CDN 上的 OnlyOffice 目录版本;未传时使用当前 SDK 版本。 */ onlyofficeVersion?: string | null; diff --git "a/src/components/onlyoffice-web-comp/docs/\346\240\270\345\277\203API.md" "b/src/components/onlyoffice-web-comp/docs/\346\240\270\345\277\203API.md" index da1ad757..7116aa35 100644 --- "a/src/components/onlyoffice-web-comp/docs/\346\240\270\345\277\203API.md" +++ "b/src/components/onlyoffice-web-comp/docs/\346\240\270\345\277\203API.md" @@ -57,7 +57,7 @@ import { // packages 根地址,目录下应包含 onlyoffice/{version}/... OnlyOfficeManager.registerStaticResource({ - cdnOrigin: "https://8057ac0e.onlyoffice-packages.pages.dev", + cdnOrigin: "https://b26830b7.onlyoffice-packages.pages.dev", }); const resource = getStaticResource(); @@ -78,7 +78,7 @@ OnlyOfficeManager.resetStaticResource(); ```typescript type OnlyOfficeStaticResourceOptions = { - /** CDN packages 根地址,例如 https://8057ac0e.onlyoffice-packages.pages.dev */ + /** CDN packages 根地址,例如 https://b26830b7.onlyoffice-packages.pages.dev */ cdnOrigin?: string | null; /** CDN 的 SDK 版本;默认当前 9.4 SDK */ onlyofficeVersion?: string | null; diff --git a/src/features/demo/resource-switcher.tsx b/src/features/demo/resource-switcher.tsx index 11882a90..1a191d3c 100644 --- a/src/features/demo/resource-switcher.tsx +++ b/src/features/demo/resource-switcher.tsx @@ -2,7 +2,7 @@ import { DemoButton, DemoField } from "./demo-toolbar"; import { OnlyOfficeManager } from "@/components/onlyoffice-web-comp"; export const DEFAULT_DEMO_CDN_ORIGIN = - "https://8057ac0e.onlyoffice-packages.pages.dev"; + "https://b26830b7.onlyoffice-packages.pages.dev"; export type DemoResourceMode = "local" | "cdn";