Files
test1/components/image-editor/utils/mark-image.ts
2026-03-20 07:33:46 +00:00

189 lines
4.7 KiB
TypeScript

export interface ImageMark {
rect: {
x: number
y: number
width: number
height: number
}
number: number
}
export function loadImage(url: string) {
return new Promise<HTMLImageElement>((resolve, reject) => {
const image = new Image()
image.crossOrigin = 'anonymous'
image.onload = () => resolve(image)
image.onerror = reject
image.src = url
})
}
export function createCanvas(width: number, height: number) {
if ('OffscreenCanvas' in window) {
return new OffscreenCanvas(width, height)
}
const canvas = document.createElement('canvas')
canvas.style.width = `${width}px`
canvas.style.height = `${height}px`
canvas.width = width
canvas.height = height
return canvas
}
export function blob2File(blob: Blob, fileName: string) {
return new File([blob], fileName, { type: blob.type })
}
export interface ImageMarkParams {
image: HTMLImageElement | HTMLCanvasElement | OffscreenCanvas
marks: ImageMark[]
blobFileType?: string
}
export async function canvas2Blob(
canvas: OffscreenCanvas | HTMLCanvasElement,
fileType?: string,
) {
if ('convertToBlob' in canvas) {
return await canvas.convertToBlob({ type: fileType })
}
return new Promise<Blob | null>(resolve => {
canvas.toBlob(blob => {
if (!blob) return resolve(null)
canvas.remove()
resolve(blob)
}, fileType)
})
}
export function image2Blob(image: HTMLImageElement) {
const canvas = createCanvas(image.width, image.height)
const ctx = canvas.getContext('2d') as
| CanvasRenderingContext2D
| OffscreenCanvasRenderingContext2D
if (!ctx) return null
ctx.drawImage(image, 0, 0, image.width, image.height)
return canvas2Blob(canvas)
}
export async function markImage(params: ImageMarkParams): Promise<Blob | null> {
const { image, marks, blobFileType: fileType = 'image/png' } = params
try {
const canvas = createCanvas(image.width, image.height)
const ctx = canvas.getContext('2d') as
| CanvasRenderingContext2D
| OffscreenCanvasRenderingContext2D
if (!ctx) return null
ctx.drawImage(image, 0, 0, image.width, image.height)
marks.forEach(mark => {
const { rect, number: markNumber } = mark
ctx.save()
/**
* 1. black-white dashed border
*/
// 1.1 black border
ctx.save()
ctx.strokeStyle = 'black'
ctx.lineWidth = 4
ctx.lineJoin = 'round'
ctx.beginPath()
const round = 6
if (ctx instanceof OffscreenCanvasRenderingContext2D) {
ctx.roundRect(rect.x, rect.y, rect.width, rect.height, round)
} else {
ctx.rect(rect.x, rect.y, rect.width, rect.height)
}
ctx.stroke()
ctx.closePath()
// 1.2 white dashed border
ctx.strokeStyle = 'white'
ctx.lineWidth = 2
ctx.setLineDash([8, 8])
ctx.lineJoin = 'round'
ctx.beginPath()
if (ctx instanceof OffscreenCanvasRenderingContext2D) {
ctx.roundRect(rect.x, rect.y, rect.width, rect.height, round)
} else {
ctx.rect(rect.x, rect.y, rect.width, rect.height)
}
ctx.stroke()
ctx.closePath()
ctx.restore()
/**
* 2. number mark
*/
// 2.1 circle background
const fontSize = 16
const radius = 16
let offsetX = 0
let offsetY = 0
if (
rect.x + rect.width + radius > image.width ||
rect.y + rect.height + radius > image.height
) {
offsetX = -radius
offsetY = -radius
}
ctx.beginPath()
ctx.arc(
rect.x + rect.width + offsetX,
rect.y + rect.height + offsetY,
radius,
0,
Math.PI * 2,
)
ctx.closePath()
ctx.fillStyle = 'blue'
ctx.fill()
// 2.2 circle border
ctx.strokeStyle = 'black'
ctx.lineWidth = 3
ctx.stroke()
ctx.strokeStyle = 'white'
ctx.lineWidth = 2
ctx.stroke()
// 2.3 number
ctx.font = `${fontSize}px Arial`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillStyle = 'white'
ctx.fillText(
markNumber.toString(),
rect.x + rect.width + offsetX,
rect.y + rect.height + offsetY + 2,
)
ctx.restore()
})
if ('convertToBlob' in canvas) {
return await (canvas as OffscreenCanvas).convertToBlob({
type: fileType,
quality: 0.8,
})
}
return new Promise<Blob | null>(resolve => {
;(canvas as HTMLCanvasElement).toBlob(blob => {
if (!blob) return resolve(null)
;(canvas as HTMLCanvasElement).remove()
resolve(blob)
}, fileType)
})
} catch (error) {
console.error('Failed to mark image:', error)
return null
}
}