| 1 | /** |
| 2 | * imageExport - 图片编辑导出配置与 Canvas 合成工具 |
| 3 | * 支持图片输出格式选择与 JPEG 质量压缩 |
| 4 | */ |
| 5 | |
| 6 | export const IMAGE_EXPORT_FORMATS = [ |
| 7 | { value: 'image/png', label: 'PNG', extension: 'png' }, |
| 8 | { value: 'image/jpeg', label: 'JPG', extension: 'jpg' }, |
| 9 | ] as const |
| 10 | |
| 11 | export const IMAGE_EXPORT_QUALITY = { |
| 12 | min: 30, |
| 13 | max: 100, |
| 14 | step: 5, |
| 15 | default: 85, |
| 16 | } as const |
| 17 | |
| 18 | export const DEFAULT_IMAGE_EXPORT_FORMAT = IMAGE_EXPORT_FORMATS[0].value |
| 19 | |
| 20 | export type ImageExportFormat = typeof IMAGE_EXPORT_FORMATS[number]['value'] |
| 21 | export type ImageExportExtension = typeof IMAGE_EXPORT_FORMATS[number]['extension'] |
| 22 | |
| 23 | export interface ImageExportOptions { |
| 24 | format: ImageExportFormat |
| 25 | quality: number |
| 26 | } |
| 27 | |
| 28 | export function getImageExportExtension(format: ImageExportFormat): ImageExportExtension { |
| 29 | return IMAGE_EXPORT_FORMATS.find(item => item.value === format)?.extension ?? 'png' |
| 30 | } |
| 31 | |
| 32 | export function getImageExportLabel(format: ImageExportFormat) { |
| 33 | return IMAGE_EXPORT_FORMATS.find(item => item.value === format)?.label ?? 'PNG' |
| 34 | } |
| 35 | |
| 36 | export function createEditedImageFileName(format: ImageExportFormat) { |
| 37 | return `edited_${Date.now()}.${getImageExportExtension(format)}` |
| 38 | } |
| 39 | |
| 40 | function getCanvasExportQuality(format: ImageExportFormat, quality: number) { |
| 41 | if (format !== 'image/jpeg') |
| 42 | return undefined |
| 43 | |
| 44 | const normalizedQuality = Math.min( |
| 45 | IMAGE_EXPORT_QUALITY.max, |
| 46 | Math.max(IMAGE_EXPORT_QUALITY.min, quality), |
| 47 | ) |
| 48 | |
| 49 | return normalizedQuality / 100 |
| 50 | } |
| 51 | |
| 52 | function fillJpegBackground(ctx: CanvasRenderingContext2D, width: number, height: number) { |
| 53 | const bodyBackground = getComputedStyle(document.body).backgroundColor |
| 54 | |
| 55 | if (bodyBackground && bodyBackground !== 'rgba(0, 0, 0, 0)') { |
| 56 | ctx.fillStyle = bodyBackground |
| 57 | } |
| 58 | else { |
| 59 | ctx.fillStyle = 'Canvas' |
| 60 | } |
| 61 | |
| 62 | ctx.fillRect(0, 0, width, height) |
| 63 | } |
| 64 | |
| 65 | export function exportCanvasLayers( |
| 66 | imageCanvas: HTMLCanvasElement, |
| 67 | drawCanvas: HTMLCanvasElement, |
| 68 | options: ImageExportOptions, |
| 69 | ): Promise<Blob> { |
| 70 | return new Promise((resolve, reject) => { |
| 71 | const mergeCanvas = document.createElement('canvas') |
| 72 | mergeCanvas.width = imageCanvas.width |
| 73 | mergeCanvas.height = imageCanvas.height |
| 74 | |
| 75 | const ctx = mergeCanvas.getContext('2d') |
| 76 | if (!ctx) { |
| 77 | reject(new Error('Failed to get canvas context')) |
| 78 | return |
| 79 | } |
| 80 | |
| 81 | if (options.format === 'image/jpeg') { |
| 82 | fillJpegBackground(ctx, mergeCanvas.width, mergeCanvas.height) |
| 83 | } |
| 84 | |
| 85 | ctx.drawImage(imageCanvas, 0, 0) |
| 86 | ctx.drawImage(drawCanvas, 0, 0) |
| 87 | |
| 88 | mergeCanvas.toBlob( |
| 89 | (blob) => { |
| 90 | if (blob) { |
| 91 | resolve(blob) |
| 92 | return |
| 93 | } |
| 94 | |
| 95 | reject(new Error('Failed to export image')) |
| 96 | }, |
| 97 | options.format, |
| 98 | getCanvasExportQuality(options.format, options.quality), |
| 99 | ) |
| 100 | }) |
| 101 | } |
| 102 |