feat:提交

This commit is contained in:
2025-07-22 18:22:31 +08:00
parent 160bb294ca
commit 2ebb3e1abe
85 changed files with 36380 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
export abstract class Drawing
{
width: number
height: number
scale: number = 1
abstract drawPath(path: DrawPathContext, option: DrawLineOption)
abstract drawLine(
x0: number,
y0: number,
x1: number,
y1: number,
option: DrawLineOption,
)
abstract drawRect(
x: number,
y: number,
width: number,
height: number,
option: DrawRectOption,
)
abstract drawLinePolygon(points: Array<any>, option: DrawLinePolygonOption)
abstract drawText(text: string, x: number, y: number, option: DrawTextOption)
abstract drawForeignObjectText(
text: string,
x: number,
y: number,
option: DrawTextOption,
)
abstract drawImage(
source: CanvasImageSource,
x: number,
y: number,
width: number,
height: number,
option: DrawImageOption,
border: number,
): Promise<any>
abstract clearAll()
abstract toHTML(): string
}
export class BaseDrawOption
{
lineWidth: number = 1
strokeStyle: string
}
interface IDrawPath
{
method: string
args: number[]
}
export class DrawPathContext
{
data: Array<IDrawPath> = []
moveTo(x: number, y: number)
{
this.data.push({ method: 'M', args: [x, y] })
return this
}
lineTo(x: number, y: number)
{
this.data.push({ method: 'L', args: [x, y] })
return this
}
}
export class DrawLineOption extends BaseDrawOption { }
export class DrawRectOption extends BaseDrawOption
{
fillStyle: string
}
export class DrawImageOption extends BaseDrawOption { }
export class DrawLinePolygonOption extends BaseDrawOption
{
isFill: boolean = true
fillStyle: string = 'rgb(0,0,0)'
}
export type DrawTextBaseLnie = 'alphabetic' | 'bottom' | 'hanging' | 'ideographic' | 'middle' | 'top'
export type DrawTextAlign = 'start' | 'center' | 'right'
export class DrawTextOption
{
maxWidth?: number
textAlign: DrawTextAlign = 'center'
textBaseline: DrawTextBaseLnie = 'middle'
fillStyle: string = 'rgb(0,0,0)'
fontSize: string = '12'
fontWeight: string = '400'
fontFamily: string = '宋体'
}

View File

@@ -0,0 +1,197 @@
import { createBmpFile } from '../bmp.js'
import { getFileExt } from '../file.js'
import type {
DrawImageOption,
DrawLineOption,
DrawLinePolygonOption,
DrawPathContext,
DrawRectOption,
DrawTextOption } from './base.js'
import
{
Drawing,
} from './base.js'
export class CanvasDrawing extends Drawing {
canvas: HTMLCanvasElement
ctx: CanvasRenderingContext2D
// isCaching: boolean;
outputType: string = 'png'
orgWidth: number
orgHeight: number
toHTML(): string {
let img = new Image(this.canvas.width, this.canvas.height)
img.src = this.canvas.toDataURL(`image/${this.outputType}`)
return img.outerHTML
}
clearAll() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
}
/**
*
*/
constructor(
canvas: HTMLCanvasElement,
// isCaching: boolean = false,
) {
super()
this.canvas = canvas
this.width = this.orgWidth = canvas.width = canvas.width
this.height = this.orgHeight = canvas.height = canvas.height
// this.isCaching = isCaching;
this.ctx = this.canvas.getContext('2d')
}
drawPath(path: DrawPathContext, option: DrawLineOption) {
this.ctx.beginPath()
for (const item of path.data) {
switch (item.method) {
case 'M':
this.ctx.moveTo(item.args[0], item.args[1])
break
case 'L':
this.ctx.lineTo(item.args[0], item.args[1])
break
}
}
this.ctx.lineWidth = option.lineWidth
this.ctx.strokeStyle = option.strokeStyle
this.ctx.stroke()
}
drawLine(
x0: number,
y0: number,
x1: number,
y1: number,
option: DrawLineOption,
) {
this.ctx.lineWidth = option.lineWidth
this.ctx.strokeStyle = option.strokeStyle
this.ctx.beginPath()
this.ctx.moveTo(x0, y0)
this.ctx.lineTo(x1, y1)
this.ctx.stroke()
}
drawRect(
x: number,
y: number,
width: number,
height: number,
option: DrawRectOption,
) {
this.ctx.lineWidth = option.lineWidth
this.ctx.strokeStyle = option.strokeStyle
this.ctx.fillStyle = option.fillStyle
if (option.fillStyle) {
this.ctx.fillRect(x, y, width, height)
} else {
this.ctx.strokeRect(x, y, width, height)
}
}
drawText(text: string, x: number, y: number, option: DrawTextOption) {
this.ctx.textAlign = option.textAlign as CanvasTextAlign
this.ctx.textBaseline = option.textBaseline as CanvasTextBaseline
this.ctx.fillStyle = option.fillStyle
this.ctx.font
= `${option.fontWeight} ${option.fontSize}px ${option.fontFamily}`
this.ctx.fillText(text, x, y, option.maxWidth)
}
drawForeignObjectText(
text: string,
x: number,
y: number,
option: DrawTextOption,
) {}
imageCache: Map<string, HTMLImageElement> = new Map()
drawImage(
source: CanvasImageSource,
x: number,
y: number,
width: number,
height: number,
option: DrawImageOption,
border: number,
) {
return new Promise((resolve, reject) => {
this.ctx.drawImage(source, x + border, y + border, width, height)
resolve(null)
})
}
drawLinePolygon(points: Array<any>, option: DrawLinePolygonOption) {
this.ctx.beginPath()
this.ctx.moveTo(points[0].x, points[0].y)
for (let i = 1; i < points.length; i++) {
this.ctx.lineTo(points[i].x, points[i].y)
}
this.ctx.closePath()
this.ctx.stroke()
if (option.isFill)
{
this.ctx.fillStyle = option.fillStyle
this.ctx.fill()
}
}
setScale(value: number) {
this.scale = value
this.canvas.width = this.width = this.orgWidth * value
this.canvas.height = this.height = this.orgHeight * value
this.ctx.scale(value, value)
}
// resize(width: number, height: number)
// {
// let oldWidth = this.width;
// let oldHeight = this.height;
// if (oldWidth != width || oldHeight != height)
// {
// let newCanvas = document.createElement('canvas');
// this.width = newCanvas.width = width;
// this.height = newCanvas.height = height;
// let newCtx = newCanvas.getContext('2d');
// newCtx.drawImage(this.canvas, 0, 0, oldWidth, oldHeight, 0, 0, width, height);
// this.canvas = newCanvas;
// this.ctx = newCtx;
// }
// }
exportFile(nameOrType: string, param: string): Promise<Blob | Uint8Array> {
let mimeType = 'image/png'
let ext = getFileExt(nameOrType)
if (ext === null) {
ext = nameOrType
}
let paramDic: { [key: string]: string } = {}
if (param != null) {
for (const item of param.split('&')) {
let kv = item.split('=')
paramDic[kv[0]] = kv[1]
}
}
switch (ext) {
case 'bmp':
let data = createBmpFile(
this.ctx.getImageData(0, 0, this.width, this.height),
paramDic,
)
return Promise.resolve(new Uint8Array(data))
case 'jpg':
case 'jpeg':
mimeType = 'image/jpeg'
break
}
return new Promise<Blob>((resolve) => {
this.canvas.toBlob((data: Blob) => {
resolve(data)
}, mimeType)
})
}
}

View File

@@ -0,0 +1,39 @@
import type { DrawTextOption } from './base'
// const canvas = new OffscreenCanvas(0, 0);
const canvas = document.createElement('canvas')
const canvasCtx = canvas.getContext('2d')
export function blobToDataURL(data: Blob): Promise<string>
{
return new Promise((resolve) =>
{
let reader = new FileReader()
reader.onload = (response) =>
{
resolve(response.target.result as string)
}
reader.readAsDataURL(data)
})
}
export function getStringWidth(
text: string,
width: number,
height: number,
option: DrawTextOption,
): number
{
canvasCtx.clearRect(
0,
0,
width,
height,
)
canvasCtx.textAlign = option.textAlign as CanvasTextAlign
canvasCtx.textBaseline = option.textBaseline as CanvasTextBaseline
canvasCtx.fillStyle = option.fillStyle
canvasCtx.font
= `${option.fontWeight} ${option.fontSize}px ${option.fontFamily}`
return canvasCtx.measureText(text).width
}

View File

@@ -0,0 +1,33 @@
import jsbarcode from 'jsbarcode'
import type { QRCodeRenderersOptions } from 'qrcode'
import { toCanvas } from 'qrcode'
// const canvas = new OffscreenCanvas(0, 0);
const canvas = document.createElement('canvas')
const canvasCtx = canvas.getContext('2d')
export async function genBarcode(text: string, options: jsbarcode.Options)
{
canvas.width = options.width
canvas.height = options.height
canvasCtx.clearRect(0, 0, options.width, options.height)
try
{
jsbarcode(canvas, text, options)
// return await canvas.transferToImageBitmap();
return await createImageBitmap(canvas)
} catch
{
return null
}
}
export async function genQrcode(text: string, options: QRCodeRenderersOptions)
{
canvas.width = options.width
canvas.height = options.width
canvasCtx.clearRect(0, 0, options.width, options.width)
await toCanvas(canvas, text, options)
// return await canvas.transferToImageBitmap();
return await createImageBitmap(canvas)
}

View File

@@ -0,0 +1,12 @@
export function UrlToBitmap(src: string)
{
return new Promise<HTMLImageElement>((resolve) =>
{
let img = new Image()
img.src = src
img.onload = () =>
{
resolve(img)
}
})
}

View File

@@ -0,0 +1,3 @@
export * from './base'
export * from './canvasDrawing'
export * from './svgDrawing'

View File

@@ -0,0 +1,251 @@
import type {
DrawImageOption, DrawLineOption, DrawLinePolygonOption, DrawPathContext, DrawRectOption,
DrawTextOption,
} from './base.js'
import
{ Drawing,
} from './base.js'
const canvas = document.createElement('canvas')
const canvasCtx = canvas.getContext('2d')
export class SvgDrawing extends Drawing
{
svg: SVGElement
/**
*
*/
constructor(
width: number | string,
height: number | string,
scale: number = 0.8,
)
{
super()
this.scale = scale
this.width = Number(width)
this.height = Number(height)
this.svg = this.createElement('svg')
// this.svg.setAttribute('width', this.width.toString());//(Number(width) * this.scale)
// this.svg.setAttribute('height', this.height.toString());//(Number(height) * this.scale)
// this.svg.style.transform = 'scale(' + this.scale + ')';
// this.svg.style.transformOrigin = 'left top';
this.svg.setAttribute('viewBox', `0 0 ${width} ${height}`)
}
private createElement(name)
{
return document.createElementNS('http://www.w3.org/2000/svg', name)
}
toHTML(): string
{
return this.svg.outerHTML
}
clearAll()
{
this.svg.innerHTML = ''
}
drawPath(path: DrawPathContext, option: DrawLineOption)
{
let el = this.createElement('path')
let pathStr = ''
for (const item of path.data)
{
switch (item.method)
{
case 'M':
pathStr += `M ${item.args.join(' ')} `
break
case 'L':
pathStr += `L ${item.args.join(' ')} `
break
}
}
el.setAttribute('d', pathStr)
el.setAttribute('stroke-width', option.lineWidth.toString())
el.setAttribute('stroke', option.strokeStyle)
this.svg.appendChild(el)
}
drawLine(
x0: number,
y0: number,
x1: number,
y1: number,
option: DrawLineOption,
)
{
let el = this.createElement('line')
el.setAttribute('x1', x0.toString()) // (Number(x0) * this.scale)
el.setAttribute('y1', y0.toString()) // (Number(y0) * this.scale)
el.setAttribute('x2', x1.toString()) // (Number(x1) * this.scale)
el.setAttribute('y2', y1.toString()) // (Number(y1) * this.scale)
el.setAttribute('stroke', option.strokeStyle)
el.setAttribute('fill', 'rgba(0,0,0,0)')
el.setAttribute('stroke-width', option.lineWidth.toString())
this.svg.appendChild(el)
}
drawRect(
x: number,
y: number,
width: number,
height: number,
option: DrawRectOption,
)
{
let el = this.createElement('rect')
el.setAttribute('x', x.toString()) // ((x + option.baseLine / 2) * this.scale)
el.setAttribute('y', y.toString()) // ((y + option.baseLine) * this.scale)
el.setAttribute('width', width.toString()) // (Number(width) * this.scale)
el.setAttribute('height', height.toString()) // (Number(height) * this.scale)
if (option.fillStyle)
{
el.setAttribute('fill', option.fillStyle)
el.setAttribute('stroke', option.strokeStyle)
} else
{
el.setAttribute('fill', 'rgba(1,1,1,0)')
el.setAttribute('stroke', option.strokeStyle)
el.setAttribute(
'stroke-width',
(Math.floor(option.lineWidth) + 0.5).toString(),
)
}
this.svg.appendChild(el)
}
drawText(text: string, x: number, y: number, option: DrawTextOption)
{
let el = this.createElement('text')
el.setAttribute('x', x.toString()) // (Number(x) * this.scale) option['CncDict'] ? (x - Number(option.fontSize) / 6).toString() :
el.setAttribute('y', y.toString()) // (Number(y) * this.scale)
el.style.dominantBaseline = 'text-before-edge'
if (option.resetFont)
{
el.style.dominantBaseline = 'middle'
el.setAttribute('text-anchor', 'middle')
}
el.setAttribute('fill', option.fillStyle)
el.setAttribute('font-size', `${option.fontSize}px`)
el.setAttribute('font-weight', option.fontWeight)
el.setAttribute('font-family', option.fontFamily)
if (option.overflowText)
{
el.setAttribute('textLength', option.maxWidth.toString())
el.setAttribute('lengthAdjust', 'spacingAndGlyphs')
}
el.setAttribute('alignment-baseline', option.textBaseline)
switch (option.textAlign)
{
case 'start':
el.setAttribute('text-anchor', 'left')
break
case 'center':
el.setAttribute('text-anchor', 'middle')
break
case 'right':
el.setAttribute('text-anchor', 'end')
break
}
if (typeof text === 'string')
{
el.innerHTML = text.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;')
} else
{
el.innerHTML = text
}
this.svg.appendChild(el)
}
drawForeignObjectText(
text: string,
x: number,
y: number,
option: DrawTextOption,
)
{
// let el = this.createElement('foreignObject');
// el.setAttribute('x',x.toString());
// el.setAttribute('y',y.toString());
// el.setAttribute('width',option.maxWidth.toString());
// el.setAttribute('height',option['maxHeight'].toString());
// let span = this.createElement('span');
// span.style.dominantBaseline = 'text-before-edge';
// if(option['resetFont']){
// span.style.dominantBaseline = 'middle';
// span.style.textAnchor = 'middle';
// }
// span.style.fontSize = option.fontSize + 'px';
// span.style.fontWeight = option.fontWeight;
// span.style.fontFamily = option.fontFamily;
// span.style.display = 'inline-block';
// span.style.width = option.maxWidth + 'px';
// span.style.height = option['maxHeight'] + 'px';
// span.style.overflow = 'hidden';
// span.innerHTML = text;
// el.appendChild(span);
// this.svg.appendChild(el);
}
async drawImage(
source: CanvasImageSource,
x: number,
y: number,
width: number,
height: number,
option: DrawImageOption,
)
{
let src = ''
if (source instanceof ImageBitmap)
{
canvas.width = source.width
canvas.height = source.height
canvasCtx.clearRect(0, 0, source.width, source.height)
canvasCtx.drawImage(source, 0, 0, source.width, source.height)
src = canvas.toDataURL()
} else if (source instanceof HTMLImageElement)
{
src = source.src
}
return new Promise((resolve, reject) =>
{
let el = this.createElement('image')
el.setAttribute('x', x.toString()) // (Number(x) * this.scale)
el.setAttribute('y', y.toString()) // (Number(y) * this.scale)
el.setAttribute('width', width.toString()) // (Number(width) * this.scale)
el.setAttribute('height', height.toString()) // (Number(height) * this.scale)
el.setAttribute('xlink:href', src)
el.setAttribute('preserveAspectRatio', 'none meet')
this.svg.appendChild(el)
resolve(null)
})
}
drawLinePolygon(points: Array<any>, option: DrawLinePolygonOption)
{
let el = this.createElement('polygon')
let pointsStr = ''
for (let i = 0; i < points.length; i++)
{
if (i == points.length - 1)
{
pointsStr += `${points[i].x} ${points[i].y}`
} else
{
pointsStr += `${points[i].x} ${points[i].y},`
}
}
el.setAttribute('points', pointsStr)
el.setAttribute('fill', option.isFill ? option.fillStyle : 'none')
el.setAttribute('stroke', option.strokeStyle)
el.setAttribute('stroke-width', option.lineWidth.toString())// 添加多段线的线宽--xyh
this.svg.appendChild(el)
}
}