// Web Worker for parallel OCR image generation // This worker uses OffscreenCanvas for true multi-threaded rendering interface RenderTask { id: number index: number text: string config: { width: number height: number textColor: string direction: string backgroundStyle: string backgroundColor: string } fontFamily: string shouldAugment: boolean augValues: Record seed: number } interface RenderResult { id: number index: number filename: string blob: Blob label: string fontName: string augmentations: string[] backgroundStyle: string isAugmented: boolean error?: string } // Seeded random for reproducibility function seededRandom(seed: number) { let s = seed return function () { s = Math.sin(s) * 10000 return s - Math.floor(s) } } // Apply augmentations to OffscreenCanvas function applyAugmentation( ctx: OffscreenCanvasRenderingContext2D, canvas: OffscreenCanvas, augValues: Record, random: () => number ): string[] { const applied: string[] = [] // Rotation if (augValues.rotation && random() > 0.5) { const angle = (random() - 0.5) * 2 * augValues.rotation * Math.PI / 180 ctx.translate(canvas.width / 2, canvas.height / 2) ctx.rotate(angle) ctx.translate(-canvas.width / 2, -canvas.height / 2) applied.push('rotation') } // Skew if (augValues.skew && random() > 0.5) { const skewAmount = (random() - 0.5) * augValues.skew * 0.01 ctx.transform(1, skewAmount, 0, 1, 0, 0) applied.push('skew') } return applied } // Render a single sample async function renderSample(task: RenderTask): Promise { const random = seededRandom(task.seed + task.index * 1000) try { // Create OffscreenCanvas const canvas = new OffscreenCanvas(task.config.width, task.config.height) const ctx = canvas.getContext('2d') if (!ctx) { throw new Error('Could not get 2d context') } // Fill background ctx.fillStyle = task.config.backgroundColor ctx.fillRect(0, 0, canvas.width, canvas.height) // Apply augmentation transforms if enabled let appliedAugmentations: string[] = [] if (task.shouldAugment) { ctx.save() appliedAugmentations = applyAugmentation(ctx, canvas, task.augValues, random) } // Set text properties const fontSize = Math.min(canvas.height * 0.6, 48) ctx.font = `${fontSize}px "${task.fontFamily}", Arial, sans-serif` ctx.fillStyle = task.config.textColor ctx.textAlign = task.config.direction === 'rtl' ? 'right' : 'left' ctx.textBaseline = 'middle' // Draw text const x = task.config.direction === 'rtl' ? canvas.width - 10 : 10 const y = canvas.height / 2 ctx.direction = task.config.direction as CanvasDirection ctx.fillText(task.text, x, y) if (task.shouldAugment) { ctx.restore() } // Post-processing augmentations if (task.shouldAugment) { // Brightness if (task.augValues.brightness && random() > 0.5) { const adjustment = 1 + (random() - 0.5) * task.augValues.brightness / 50 const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height) for (let i = 0; i < imageData.data.length; i += 4) { imageData.data[i] = Math.min(255, imageData.data[i] * adjustment) imageData.data[i + 1] = Math.min(255, imageData.data[i + 1] * adjustment) imageData.data[i + 2] = Math.min(255, imageData.data[i + 2] * adjustment) } ctx.putImageData(imageData, 0, 0) appliedAugmentations.push('brightness') } // Noise if (task.augValues.gaussian_noise && random() > 0.6) { const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height) const noiseLevel = task.augValues.gaussian_noise / 2 for (let i = 0; i < imageData.data.length; i += 4) { const noise = (random() - 0.5) * noiseLevel imageData.data[i] = Math.max(0, Math.min(255, imageData.data[i] + noise)) imageData.data[i + 1] = Math.max(0, Math.min(255, imageData.data[i + 1] + noise)) imageData.data[i + 2] = Math.max(0, Math.min(255, imageData.data[i + 2] + noise)) } ctx.putImageData(imageData, 0, 0) appliedAugmentations.push('noise') } } // Convert to blob const blob = await canvas.convertToBlob({ type: 'image/png' }) const filename = `image_${String(task.index).padStart(6, '0')}.png` return { id: task.id, index: task.index, filename, blob, label: `${filename}\t${task.text}`, fontName: task.fontFamily, augmentations: appliedAugmentations, backgroundStyle: task.config.backgroundStyle, isAugmented: task.shouldAugment } } catch (error) { return { id: task.id, index: task.index, filename: '', blob: new Blob(), label: '', fontName: '', augmentations: [], backgroundStyle: '', isAugmented: false, error: error instanceof Error ? error.message : 'Unknown error' } } } // Handle messages from main thread self.onmessage = async (e: MessageEvent) => { const { type, tasks } = e.data if (type === 'render') { // Process all tasks in this batch const results: RenderResult[] = [] for (const task of tasks as RenderTask[]) { const result = await renderSample(task) results.push(result) // Send progress for each completed task self.postMessage({ type: 'progress', result }) } // Send completion signal self.postMessage({ type: 'complete', results }) } } // Signal that worker is ready self.postMessage({ type: 'ready' })