Spaces:
Running
Running
| importScripts('https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js'); | |
| self.onmessage = async function(e) { | |
| const { type, payload } = e.data; | |
| if (type === 'EXPORT_ZIP') { | |
| const { items, zipName } = payload; | |
| try { | |
| const zip = new JSZip(); | |
| const usedNames = new Set(); | |
| for (let i = 0; i < items.length; i++) { | |
| const item = items[i]; | |
| self.postMessage({ type: 'PROGRESS', text: `处理图片 ${i + 1}/${items.length}`, percent: (i / items.length) * 50 }); | |
| // Decode image efficiently | |
| const bmp = await createImageBitmap(item.file); | |
| const canvas = new OffscreenCanvas(bmp.width, bmp.height); | |
| const ctx = canvas.getContext('2d'); | |
| ctx.drawImage(bmp, 0, 0); | |
| // Draw Watermark | |
| drawWatermarkWorker(ctx, canvas.width, canvas.height, item.data); | |
| // Convert to Blob | |
| const blob = await canvas.convertToBlob({ type: 'image/jpeg', quality: 0.9 }); | |
| bmp.close(); // Free memory | |
| // Prepare filename | |
| const data = item.data; | |
| const sanitize = (str) => (str || '').replace(/[\\/:*?"<>|]/g, '_'); | |
| const cinema = sanitize(data.cinema); | |
| const movie = sanitize(data.movie); | |
| let formattedTime = data.time.replace(/\s+/g, '_').replace(/:/g, '-'); | |
| const tickets = sanitize(data.tickets); | |
| const baseName = `${cinema}_${movie}_${formattedTime}_共出票${tickets}张`; | |
| let finalName = baseName; | |
| let counter = 1; | |
| while (usedNames.has(finalName)) { | |
| counter++; | |
| finalName = `${baseName}_${counter}`; | |
| } | |
| usedNames.add(finalName); | |
| zip.file(`${finalName}.jpg`, blob); | |
| } | |
| self.postMessage({ type: 'PROGRESS', text: '正在生成 ZIP 文件...', percent: 50 }); | |
| const content = await zip.generateAsync({ | |
| type: "blob", | |
| compression: "STORE" // Faster than DEFLATE, fine for JPEGs | |
| }, (metadata) => { | |
| self.postMessage({ | |
| type: 'PROGRESS', | |
| text: `正在压缩... ${metadata.percent.toFixed(0)}%`, | |
| percent: 50 + (metadata.percent * 0.5) | |
| }); | |
| }); | |
| self.postMessage({ type: 'DONE', blob: content, filename: zipName }); | |
| } catch (error) { | |
| self.postMessage({ type: 'ERROR', error: error.message }); | |
| } | |
| } else if (type === 'EXPORT_SINGLE') { | |
| const { item, filename } = payload; | |
| try { | |
| self.postMessage({ type: 'PROGRESS', text: '处理中...', percent: 50 }); | |
| const bmp = await createImageBitmap(item.file); | |
| const canvas = new OffscreenCanvas(bmp.width, bmp.height); | |
| const ctx = canvas.getContext('2d'); | |
| ctx.drawImage(bmp, 0, 0); | |
| drawWatermarkWorker(ctx, canvas.width, canvas.height, item.data); | |
| const blob = await canvas.convertToBlob({ type: 'image/jpeg', quality: 0.9 }); | |
| bmp.close(); | |
| self.postMessage({ type: 'DONE_SINGLE', blob, filename }); | |
| } catch(error) { | |
| self.postMessage({ type: 'ERROR', error: error.message }); | |
| } | |
| } else if (type === 'EXPORT_ALL_SINGLE') { | |
| const { items } = payload; | |
| try { | |
| for (let i = 0; i < items.length; i++) { | |
| const item = items[i]; | |
| self.postMessage({ type: 'PROGRESS', text: `导出图片 ${i + 1}/${items.length}`, percent: (i / items.length) * 100 }); | |
| const bmp = await createImageBitmap(item.file); | |
| const canvas = new OffscreenCanvas(bmp.width, bmp.height); | |
| const ctx = canvas.getContext('2d'); | |
| ctx.drawImage(bmp, 0, 0); | |
| drawWatermarkWorker(ctx, canvas.width, canvas.height, item.data); | |
| const blob = await canvas.convertToBlob({ type: 'image/jpeg', quality: 0.9 }); | |
| bmp.close(); | |
| const data = item.data; | |
| const sanitize = (str) => (str || '').replace(/[\\/:*?"<>|]/g, '_'); | |
| const cinema = sanitize(data.cinema); | |
| const movie = sanitize(data.movie); | |
| let formattedTime = data.time.replace(/\s+/g, '_').replace(/:/g, '-'); | |
| const tickets = sanitize(data.tickets); | |
| const newName = `${cinema}_${movie}_${formattedTime}_共出票${tickets}张_${i+1}.jpg`; | |
| self.postMessage({ type: 'DONE_SINGLE', blob, filename: newName }); | |
| } | |
| self.postMessage({ type: 'DONE_ALL_SINGLE' }); | |
| } catch(error) { | |
| self.postMessage({ type: 'ERROR', error: error.message }); | |
| } | |
| } | |
| }; | |
| function drawWatermarkWorker(ctx, width, height, data) { | |
| const baseFontSize = Math.max(30, Math.floor(height * 0.03)); | |
| const fontSize = baseFontSize * (data.sizeScale || 1.0); | |
| ctx.font = `bold ${fontSize}px sans-serif`; | |
| ctx.textAlign = 'center'; | |
| ctx.fillStyle = data.color || '#ffffff'; | |
| ctx.strokeStyle = '#000000'; | |
| ctx.lineWidth = Math.max(2, Math.floor(fontSize / 15)); | |
| const movieTitle = data.movie ? `《${data.movie}》` : ''; | |
| const lines = [ | |
| data.cinema, | |
| movieTitle, | |
| data.time, | |
| data.tickets ? `共出票${data.tickets}张` : '', | |
| data.extra1, | |
| data.extra2 | |
| ]; | |
| const validLines = lines.filter(line => line && line.trim() !== ''); | |
| const xOffset = (data.offsetX || 0) * (width / 1000); | |
| const yOffset = (data.offsetY || 0) * (height / 1000); | |
| const lineHeight = fontSize * 1.5; | |
| const totalTextHeight = validLines.length * lineHeight; | |
| const startY = height - totalTextHeight - (height * 0.05) + yOffset; | |
| const x = (width / 2) + xOffset; | |
| for (let index = 0; index < validLines.length; index++) { | |
| const line = validLines[index]; | |
| const y = startY + (index * lineHeight) + fontSize; | |
| ctx.strokeText(line, x, y); | |
| ctx.fillText(line, x, y); | |
| } | |
| } |