labelEditorFonts.ts 7.49 KB
import type { SystemLabelTemplate } from './print/types/printer'

/** 与 Web 管理端 labelTemplate.ts 保持一致 */
export const LABEL_EDITOR_FONT_FAMILY = 'FreightSans Bold'

export const LABEL_EDITOR_FONT_OPTIONS: ReadonlyArray<{ value: string; label: string }> = [
  { value: 'FreightSans Bold', label: 'FreightSans Bold' },
  { value: 'Roboto', label: 'Roboto' },
  { value: 'Open Sans', label: 'Open Sans' },
  { value: 'Lato', label: 'Lato' },
  { value: 'Tinos', label: 'Tinos' },
  { value: 'Roboto Mono', label: 'Roboto Mono' },
]

export const LABEL_EDITOR_BUNDLED_FONT_FAMILIES = new Set(
  LABEL_EDITOR_FONT_OPTIONS.map((item) => item.value),
)

const LEGACY_FONT_ALIASES: Record<string, string> = {
  arial: 'Roboto',
  'arial, sans-serif': 'Roboto',
  helvetica: 'Roboto',
  'helvetica, sans-serif': 'Roboto',
  'times new roman': 'Tinos',
  'courier new': 'Roboto Mono',
  verdana: 'Open Sans',
  georgia: 'Tinos',
}

type FontVariantSpec = {
  file: string
  weight: 'normal' | 'bold'
  style: 'normal' | 'italic'
}

type FontPackageSpec = {
  family: string
  dir: string
  variants: FontVariantSpec[]
}

/** static/fonts 下已打包字体(与 Web src/assets/fonts 对齐) */
export const LABEL_EDITOR_FONT_PACKAGES: FontPackageSpec[] = [
  {
    family: 'FreightSans Bold',
    dir: 'freight-sans-bold',
    variants: [{ file: 'FreightSans-Bold.ttf', weight: 'bold', style: 'normal' }],
  },
  {
    family: 'Roboto',
    dir: 'roboto',
    variants: [
      { file: 'roboto-latin-400-normal.woff2', weight: 'normal', style: 'normal' },
      { file: 'roboto-latin-700-normal.woff2', weight: 'bold', style: 'normal' },
      { file: 'roboto-latin-400-italic.woff2', weight: 'normal', style: 'italic' },
    ],
  },
  {
    family: 'Open Sans',
    dir: 'open-sans',
    variants: [
      { file: 'open-sans-latin-400-normal.woff2', weight: 'normal', style: 'normal' },
      { file: 'open-sans-latin-700-normal.woff2', weight: 'bold', style: 'normal' },
      { file: 'open-sans-latin-400-italic.woff2', weight: 'normal', style: 'italic' },
    ],
  },
  {
    family: 'Lato',
    dir: 'lato',
    variants: [
      { file: 'lato-latin-400-normal.woff2', weight: 'normal', style: 'normal' },
      { file: 'lato-latin-700-normal.woff2', weight: 'bold', style: 'normal' },
      { file: 'lato-latin-400-italic.woff2', weight: 'normal', style: 'italic' },
    ],
  },
  {
    family: 'Tinos',
    dir: 'tinos',
    variants: [
      { file: 'tinos-latin-400-normal.woff2', weight: 'normal', style: 'normal' },
      { file: 'tinos-latin-700-normal.woff2', weight: 'bold', style: 'normal' },
      { file: 'tinos-latin-400-italic.woff2', weight: 'normal', style: 'italic' },
    ],
  },
  {
    family: 'Roboto Mono',
    dir: 'roboto-mono',
    variants: [
      { file: 'roboto-mono-latin-400-normal.woff2', weight: 'normal', style: 'normal' },
      { file: 'roboto-mono-latin-700-normal.woff2', weight: 'bold', style: 'normal' },
      { file: 'roboto-mono-latin-400-italic.woff2', weight: 'normal', style: 'italic' },
    ],
  },
]

const loadedFaceKeys = new Set<string>()
const loadedFamilies = new Set<string>()
let preloadAllPromise: Promise<void> | null = null

export function normalizeLabelEditorFontFamily(raw: unknown): string | null {
  const text = String(raw ?? '').trim()
  if (!text) return null
  const alias = LEGACY_FONT_ALIASES[text.toLowerCase()]
  if (alias) return alias
  if (LABEL_EDITOR_BUNDLED_FONT_FAMILIES.has(text)) return text
  return null
}

export function resolveLabelEditorFontFamily(
  config: Record<string, unknown> | null | undefined,
): string {
  return (
    normalizeLabelEditorFontFamily(config?.fontFamily ?? config?.FontFamily)
    ?? LABEL_EDITOR_FONT_FAMILY
  )
}

function fontPackageByFamily(family: string): FontPackageSpec | undefined {
  return LABEL_EDITOR_FONT_PACKAGES.find((item) => item.family === family)
}

/** static/fonts/{dir}/{file} → 各端可访问的本地 URL */
export function resolveStaticFontFileUrl(dir: string, file: string): string {
  const rel = `static/fonts/${dir}/${file}`.replace(/\\/g, '/')
  // #ifdef APP-PLUS
  try {
    if (typeof plus !== 'undefined' && plus.io?.convertLocalFileSystemURL) {
      return plus.io.convertLocalFileSystemURL(`_www/${rel}`)
    }
  } catch (_) {
    /* fall through */
  }
  // #endif
  return `/${rel}`
}

/** Android 原生位图文字:Typeface.createFromFile 用的绝对路径 */
export function resolveAndroidFontFilePath(
  family: string,
  bold: boolean,
  italic: boolean,
): string | null {
  const pkg = fontPackageByFamily(family)
  if (!pkg) return null
  const weight = bold ? 'bold' : 'normal'
  const style = italic ? 'italic' : 'normal'
  let variant = pkg.variants.find((v) => v.weight === weight && v.style === style)
  if (!variant && bold) {
    variant = pkg.variants.find((v) => v.weight === 'bold' && v.style === 'normal')
  }
  if (!variant) {
    variant = pkg.variants.find((v) => v.weight === 'normal' && v.style === 'normal')
  }
  if (!variant) return null
  return resolveStaticFontFileUrl(pkg.dir, variant.file)
}

function loadFontFaceOnce(
  family: string,
  dir: string,
  variant: FontVariantSpec,
): Promise<void> {
  const key = `${family}|${variant.weight}|${variant.style}|${variant.file}`
  if (loadedFaceKeys.has(key)) return Promise.resolve()
  const url = resolveStaticFontFileUrl(dir, variant.file)
  return new Promise((resolve) => {
    uni.loadFontFace({
      global: true,
      family,
      source: `url("${url}")`,
      desc: {
        style: variant.style,
        weight: variant.weight,
      },
      success: () => {
        loadedFaceKeys.add(key)
        loadedFamilies.add(family)
        if (variant.style === 'italic') loadedItalicFamilies.add(family)
        resolve()
      },
      fail: (err) => {
        console.warn('[labelEditorFonts] loadFontFace failed', family, variant, err)
        resolve()
      },
    })
  })
}

export function isLabelEditorFontFamilyLoaded(family: string): boolean {
  return loadedFamilies.has(family)
}

const loadedItalicFamilies = new Set<string>()

export function isLabelEditorFontItalicLoaded(family: string): boolean {
  return loadedItalicFamilies.has(family)
}

export async function ensureLabelEditorFontFamilyLoaded(family: string): Promise<void> {
  const normalized = normalizeLabelEditorFontFamily(family) ?? LABEL_EDITOR_FONT_FAMILY
  const pkg = fontPackageByFamily(normalized)
  if (!pkg) return
  await Promise.all(
    pkg.variants.map((variant) => loadFontFaceOnce(pkg.family, pkg.dir, variant)),
  )
}

export function collectFontFamiliesFromTemplate(template: SystemLabelTemplate): string[] {
  const set = new Set<string>([LABEL_EDITOR_FONT_FAMILY])
  for (const el of template.elements || []) {
    const cfg = (el.config || {}) as Record<string, unknown>
    const family = normalizeLabelEditorFontFamily(cfg.fontFamily ?? cfg.FontFamily)
    if (family) set.add(family)
  }
  return [...set]
}

/** 预览 / 打印 canvas 绘制前调用,加载模板用到的字体 */
export async function ensureLabelEditorFontsForTemplate(
  template: SystemLabelTemplate,
): Promise<void> {
  const families = collectFontFamiliesFromTemplate(template)
  await Promise.all(families.map((family) => ensureLabelEditorFontFamilyLoaded(family)))
}

/** App 启动后后台预加载全部打包字体,减少首次预览等待 */
export function preloadAllLabelEditorFonts(): Promise<void> {
  if (!preloadAllPromise) {
    preloadAllPromise = Promise.all(
      LABEL_EDITOR_FONT_PACKAGES.map((pkg) => ensureLabelEditorFontFamilyLoaded(pkg.family)),
    ).then(() => undefined)
  }
  return preloadAllPromise
}