From f702db81dd85ed95422d20e8fafe7964ec0e769f Mon Sep 17 00:00:00 2001 From: wengxiangmin <157215725@qq.com> Date: Wed, 6 Nov 2024 17:27:32 +0800 Subject: [PATCH] feat: support font minify using external lib --- src/clone-node.ts | 51 +++++++++++++++++++++++++++++++++------- src/context.ts | 4 ++-- src/copy-pseudo-class.ts | 3 +++ src/create-context.ts | 2 +- src/fetch.ts | 23 +++++++++++++++--- src/options.ts | 5 ++++ 6 files changed, 74 insertions(+), 14 deletions(-) diff --git a/src/clone-node.ts b/src/clone-node.ts index b885cc5..5df91a8 100644 --- a/src/clone-node.ts +++ b/src/clone-node.ts @@ -25,6 +25,7 @@ async function appendChildNode( cloned: T, child: ChildNode, context: Context, + addWordToFontFamilies?: (text: string) => void, ): Promise { if (isElementNode(child) && (isStyleElement(child) || isScriptElement(child))) return @@ -42,7 +43,7 @@ async function appendChildNode( context.currentParentNodeStyle = context.currentNodeStyle } - const childCloned = await cloneNode(child, context) + const childCloned = await cloneNode(child, context, false, addWordToFontFamilies) if (context.isEnable('restoreScrollPosition')) { restoreScrollPosition(node, childCloned) @@ -55,6 +56,7 @@ async function cloneChildNodes( node: T, cloned: T, context: Context, + addWordToFontFamilies?: (text: string) => void, ): Promise { const firstChild = ( isElementNode(node) @@ -72,11 +74,11 @@ async function cloneChildNodes( ) { const nodes = child.assignedNodes() for (let i = 0; i < nodes.length; i++) { - await appendChildNode(node, cloned, nodes[i] as ChildNode, context) + await appendChildNode(node, cloned, nodes[i] as ChildNode, context, addWordToFontFamilies) } } else { - await appendChildNode(node, cloned, child, context) + await appendChildNode(node, cloned, child, context, addWordToFontFamilies) } } } @@ -135,10 +137,14 @@ export async function cloneNode( node: T, context: Context, isRoot = false, + addWordToFontFamilies?: (text: string) => void, ): Promise { const { ownerDocument, ownerWindow, fontFamilies } = context if (ownerDocument && isTextNode(node)) { + if (addWordToFontFamilies && /\S/.test(node.data)) { + addWordToFontFamilies(node.data) + } return ownerDocument.createTextNode(node.data) } @@ -180,15 +186,44 @@ export async function cloneNode( ) } - copyPseudoClass(node, cloned, copyScrollbar, context) + const textTransform = style.get('text-transform')?.[0] + const families = splitFontFamily(style.get('font-family')?.[0]) + const addWordToFontFamilies = families + ? (word: string) => { + if (textTransform === 'uppercase') { + word = word.toUpperCase() + } + else if (textTransform === 'lowercase') { + word = word.toLowerCase() + } + else if (textTransform === 'capitalize') { + word = word[0].toUpperCase() + word.substring(1) + } + families!.forEach((family) => { + let fontFamily = fontFamilies.get(family) + if (!fontFamily) { + fontFamilies.set(family, fontFamily = new Set()) + } + word.split('').forEach(text => fontFamily!.add(text)) + }) + } + : undefined + + copyPseudoClass( + node, + cloned, + copyScrollbar, + context, + addWordToFontFamilies, + ) copyInputValue(node, cloned) - splitFontFamily(style.get('font-family')?.[0]) - ?.forEach(val => fontFamilies.add(val)) - if (!isVideoElement(node)) { - await cloneChildNodes(node, cloned, context) + await cloneChildNodes( + node, cloned, context, + addWordToFontFamilies, + ) } return cloned diff --git a/src/context.ts b/src/context.ts index be7f9bb..b4f82aa 100644 --- a/src/context.ts +++ b/src/context.ts @@ -77,9 +77,9 @@ export interface InternalContext { workers: Worker[] /** - * The set of `font-family` values for all cloned elements + * The map of `font-family` values for all cloend elements */ - fontFamilies: Set + fontFamilies: Map> /** * Map diff --git a/src/copy-pseudo-class.ts b/src/copy-pseudo-class.ts index e8efbdc..ce62620 100644 --- a/src/copy-pseudo-class.ts +++ b/src/copy-pseudo-class.ts @@ -26,6 +26,7 @@ export function copyPseudoClass( cloned: T, copyScrollbar: boolean, context: Context, + addWordToFontFamilies?: (text: string) => void, ): void { const { ownerWindow, svgStyleElement, svgStyles, currentNodeStyle } = context @@ -39,6 +40,8 @@ export function copyPseudoClass( if (!content || content === 'none') return + addWordToFontFamilies?.(content) + content = content // TODO support css.counter .replace(/(')|(")|(counter\(.+\))/g, '') diff --git a/src/create-context.ts b/src/create-context.ts index a62a596..c973fee 100644 --- a/src/create-context.ts +++ b/src/create-context.ts @@ -101,7 +101,7 @@ export async function createContext(node: T, options?: Options & return null } }).filter(Boolean) as any, - fontFamilies: new Set(), + fontFamilies: new Map(), fontCssTexts: new Map(), acceptOfImage: `${[ supportWebp(ownerDocument) && 'image/webp', diff --git a/src/fetch.ts b/src/fetch.ts index 972cee5..a3cc084 100644 --- a/src/fetch.ts +++ b/src/fetch.ts @@ -4,7 +4,7 @@ import { blobToDataUrl, consoleWarn, IN_FIREFOX, IN_SAFARI } from './utils' export type BaseFetchOptions = RequestInit & { url: string timeout?: number - responseType?: 'text' | 'dataUrl' + responseType?: 'text' | 'dataUrl' | 'arrayBuffer' } export type ContextFetchOptions = BaseFetchOptions & { @@ -12,7 +12,7 @@ export type ContextFetchOptions = BaseFetchOptions & { imageDom?: HTMLImageElement | SVGImageElement } -export function baseFetch(options: BaseFetchOptions): Promise { +export function baseFetch(options: BaseFetchOptions): Promise { const { url, timeout, responseType, ...requestInit } = options const controller = new AbortController() @@ -27,6 +27,8 @@ export function baseFetch(options: BaseFetchOptions): Promise { throw new Error('Failed fetch, not 2xx response', { cause: response }) } switch (responseType) { + case 'arrayBuffer': + return response.arrayBuffer() as any case 'dataUrl': return response.blob().then(blobToDataUrl) case 'text': @@ -51,7 +53,9 @@ export function contextFetch(context: Context, options: ContextFetchOptions): Pr bypassingCache, placeholderImage, }, + font, workers, + fontFamilies, } = context if (requestType === 'image' && (IN_SAFARI || IN_FIREFOX)) { @@ -68,10 +72,23 @@ export function contextFetch(context: Context, options: ContextFetchOptions): Pr } } + // Font minify + const canFontMinify = requestType.startsWith('font') && font && font.minify + const fontTexts = new Set() + if (canFontMinify) { + const families = requestType.split(';')[1].split(',') + families.forEach((family) => { + if (!fontFamilies.has(family)) + return + fontFamilies.get(family)!.forEach(text => fontTexts.add(text)) + }) + } + const needFontMinify = canFontMinify && fontTexts.size + const baseFetchOptions: BaseFetchOptions = { url, timeout, - responseType, + responseType: needFontMinify ? 'arrayBuffer' : responseType, headers: requestType === 'image' ? { accept: acceptOfImage } : undefined, ...requestInit, } diff --git a/src/options.ts b/src/options.ts index 75bcd9f..cc7f14e 100644 --- a/src/options.ts +++ b/src/options.ts @@ -121,6 +121,11 @@ export interface Options { * The options of fonts download and embed. */ font?: false | { + /** + * Font minify + */ + minify?: (font: ArrayBuffer, subset: string) => ArrayBuffer + /** * The preferred font format. If specified all other font formats are ignored. */