mirror of
https://github.com/duhanbalci/dreport.git
synced 2026-07-01 18:39:16 +00:00
195 lines
6.1 KiB
TypeScript
195 lines
6.1 KiB
TypeScript
/// Layout Engine Web Worker
|
||
/// Template JSON + Data JSON → Layout WASM → LayoutResult
|
||
/// Font loading is dynamic — fetches from backend API based on template needs.
|
||
|
||
import init, { loadFonts, addFonts, computeLayout, generateBarcode } from '../core/wasm-layout/dreport_layout.js'
|
||
import type { LayoutResult } from '../core/layout-types'
|
||
|
||
let initPromise: Promise<void> | null = null
|
||
|
||
/** Configurable font API base URL. Default: same origin /api/fonts */
|
||
let fontApiBase = '/api/fonts'
|
||
|
||
/** Font catalog from backend API */
|
||
interface FontVariantInfo {
|
||
weight: number
|
||
italic: boolean
|
||
}
|
||
interface FontFamilyInfo {
|
||
family: string
|
||
variants: FontVariantInfo[]
|
||
}
|
||
let fontCatalog: FontFamilyInfo[] = []
|
||
|
||
/** Track which font families are already loaded into WASM */
|
||
const loadedFamilies = new Set<string>()
|
||
|
||
async function doInit() {
|
||
console.log('[layout-worker] WASM başlatılıyor...')
|
||
await init({ module_or_path: '/wasm/dreport_layout_bg.wasm' })
|
||
|
||
// Fetch font catalog from backend
|
||
try {
|
||
const res = await fetch(fontApiBase)
|
||
if (res.ok) {
|
||
fontCatalog = await res.json()
|
||
console.log(`[layout-worker] Font kataloğu yüklendi (${fontCatalog.length} aile)`)
|
||
} else {
|
||
console.warn(`[layout-worker] Font kataloğu alınamadı (HTTP ${res.status}), static fallback deneniyor`)
|
||
await loadStaticFallback()
|
||
return
|
||
}
|
||
} catch {
|
||
console.warn('[layout-worker] Font API erişilemedi, static fallback deneniyor')
|
||
await loadStaticFallback()
|
||
return
|
||
}
|
||
|
||
// Load default fonts (Noto Sans + Noto Sans Mono)
|
||
await ensureFamiliesLoaded(['Noto Sans', 'Noto Sans Mono'])
|
||
console.log('[layout-worker] Hazır')
|
||
}
|
||
|
||
/** Fallback: load fonts from static /fonts/ directory (backwards compat) */
|
||
async function loadStaticFallback() {
|
||
const STATIC_FONTS = [
|
||
'/fonts/NotoSans-Regular.ttf',
|
||
'/fonts/NotoSans-Bold.ttf',
|
||
'/fonts/NotoSans-Italic.ttf',
|
||
'/fonts/NotoSans-BoldItalic.ttf',
|
||
'/fonts/NotoSansMono-Regular.ttf',
|
||
]
|
||
|
||
const buffers: Uint8Array[] = []
|
||
await Promise.all(
|
||
STATIC_FONTS.map(async (path) => {
|
||
const url = new URL(path, self.location.origin).href
|
||
const res = await fetch(url)
|
||
if (res.ok) {
|
||
buffers.push(new Uint8Array(await res.arrayBuffer()))
|
||
}
|
||
})
|
||
)
|
||
|
||
if (buffers.length > 0) {
|
||
loadFonts(buffers)
|
||
loadedFamilies.add('noto sans')
|
||
loadedFamilies.add('noto sans mono')
|
||
console.log(`[layout-worker] Static fallback: ${buffers.length} font yüklendi`)
|
||
}
|
||
}
|
||
|
||
/** Load all variants of given families from the API into WASM */
|
||
async function ensureFamiliesLoaded(families: string[]): Promise<void> {
|
||
const toLoad = families.filter(f => !loadedFamilies.has(f.toLowerCase()))
|
||
if (toLoad.length === 0) return
|
||
|
||
const buffers: Uint8Array[] = []
|
||
|
||
for (const family of toLoad) {
|
||
const info = fontCatalog.find(f => f.family.toLowerCase() === family.toLowerCase())
|
||
if (!info) {
|
||
console.warn(`[layout-worker] Font ailesi bulunamadı: ${family}`)
|
||
continue
|
||
}
|
||
|
||
const fetches = info.variants.map(async (v) => {
|
||
const url = `${fontApiBase}/${encodeURIComponent(info.family)}/${v.weight}/${v.italic}`
|
||
const res = await fetch(url)
|
||
if (res.ok) {
|
||
return new Uint8Array(await res.arrayBuffer())
|
||
}
|
||
return null
|
||
})
|
||
|
||
const results = await Promise.all(fetches)
|
||
for (const buf of results) {
|
||
if (buf && buf.byteLength > 0) {
|
||
buffers.push(buf)
|
||
}
|
||
}
|
||
loadedFamilies.add(family.toLowerCase())
|
||
}
|
||
|
||
if (buffers.length > 0) {
|
||
if (loadedFamilies.size <= toLoad.length) {
|
||
// First load — use loadFonts
|
||
loadFonts(buffers)
|
||
} else {
|
||
// Subsequent loads — use addFonts
|
||
addFonts(buffers)
|
||
}
|
||
console.log(`[layout-worker] ${toLoad.join(', ')} yüklendi (${buffers.length} variant)`)
|
||
}
|
||
}
|
||
|
||
function ensureInit(): Promise<void> {
|
||
if (!initPromise) {
|
||
initPromise = doInit()
|
||
}
|
||
return initPromise
|
||
}
|
||
|
||
type WorkerMessage =
|
||
| { type: 'compile'; templateJson: string; dataJson: string; id: number }
|
||
| { type: 'barcode'; format: string; value: string; width: number; height: number; includeText: boolean; id: number }
|
||
| { type: 'configure'; fontApiBase?: string }
|
||
|
||
self.onmessage = async (e: MessageEvent<WorkerMessage>) => {
|
||
const msg = e.data
|
||
|
||
if (msg.type === 'configure') {
|
||
if (msg.fontApiBase) {
|
||
fontApiBase = msg.fontApiBase
|
||
}
|
||
return
|
||
}
|
||
|
||
if (msg.type === 'compile') {
|
||
try {
|
||
await ensureInit()
|
||
|
||
// Extract font families from template and ensure they're loaded
|
||
try {
|
||
const tpl = JSON.parse(msg.templateJson)
|
||
if (Array.isArray(tpl.fonts) && tpl.fonts.length > 0) {
|
||
await ensureFamiliesLoaded(tpl.fonts)
|
||
}
|
||
} catch {
|
||
// Template parse failure will be caught by computeLayout below
|
||
}
|
||
|
||
const t0 = performance.now()
|
||
const resultJson = computeLayout(msg.templateJson, msg.dataJson)
|
||
const layout: LayoutResult = JSON.parse(resultJson)
|
||
console.log(`[layout-worker] render ${(performance.now() - t0).toFixed(1)}ms`)
|
||
|
||
self.postMessage({ type: 'result', layout, id: msg.id })
|
||
} catch (err) {
|
||
const errorMsg = err instanceof Error ? err.message : String(err)
|
||
console.error(`[layout-worker] Hata (id: ${msg.id}):`, err)
|
||
self.postMessage({ type: 'error', error: errorMsg, id: msg.id })
|
||
}
|
||
} else if (msg.type === 'barcode') {
|
||
try {
|
||
await ensureInit()
|
||
|
||
const raw = generateBarcode(msg.format, msg.value, msg.width, msg.height, msg.includeText)
|
||
// İlk 8 byte header: width (4 byte LE) + height (4 byte LE)
|
||
const dv = new DataView(raw.buffer, raw.byteOffset, 8)
|
||
const w = dv.getUint32(0, true)
|
||
const h = dv.getUint32(4, true)
|
||
const rgba = raw.slice(8)
|
||
|
||
self.postMessage(
|
||
{ type: 'barcode-result', width: w, height: h, rgba: rgba.buffer, id: msg.id },
|
||
[rgba.buffer] as any,
|
||
)
|
||
} catch (err) {
|
||
const errorMsg = err instanceof Error ? err.message : String(err)
|
||
console.error(`[layout-worker] Barcode hatası (id: ${msg.id}):`, err)
|
||
self.postMessage({ type: 'barcode-error', error: errorMsg, id: msg.id })
|
||
}
|
||
}
|
||
}
|