189 lines
4.7 KiB
TypeScript
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
|
|
}
|
|
}
|