Spaces:
Build error
Build error
| import { singleton, container } from 'tsyringe'; | |
| import { AsyncService, mimeOf, ParamValidationError, SubmittedDataMalformedError, /* downloadFile */ } from 'civkit'; | |
| import { readFile } from 'fs/promises'; | |
| import type canvas from '@napi-rs/canvas'; | |
| export type { Canvas, Image } from '@napi-rs/canvas'; | |
| import { GlobalLogger } from './logger'; | |
| import { TempFileManager } from './temp-file'; | |
| import { isMainThread } from 'worker_threads'; | |
| import type { svg2png } from 'svg2png-wasm' with { 'resolution-mode': 'import' }; | |
| import path from 'path'; | |
| import { Threaded } from './threaded'; | |
| const downloadFile = async (uri: string) => { | |
| const resp = await fetch(uri); | |
| if (!(resp.ok && resp.body)) { | |
| throw new Error(`Unexpected response ${resp.statusText}`); | |
| } | |
| const contentLength = parseInt(resp.headers.get('content-length') || '0'); | |
| if (contentLength > 1024 * 1024 * 100) { | |
| throw new Error('File too large'); | |
| } | |
| const buff = await resp.arrayBuffer(); | |
| return { buff, contentType: resp.headers.get('content-type') }; | |
| }; | |
| () | |
| export class CanvasService extends AsyncService { | |
| logger = this.globalLogger.child({ service: this.constructor.name }); | |
| svg2png!: typeof svg2png; | |
| canvas!: typeof canvas; | |
| constructor( | |
| protected temp: TempFileManager, | |
| protected globalLogger: GlobalLogger, | |
| ) { | |
| super(...arguments); | |
| } | |
| override async init() { | |
| await this.dependencyReady(); | |
| if (!isMainThread) { | |
| const { createSvg2png, initialize } = require('svg2png-wasm'); | |
| const wasmBuff = await readFile(path.resolve(path.dirname(require.resolve('svg2png-wasm')), '../svg2png_wasm_bg.wasm')); | |
| const fontBuff = await readFile(path.resolve(__dirname, '../../licensed/SourceHanSansSC-Regular.otf')); | |
| await initialize(wasmBuff); | |
| this.svg2png = createSvg2png({ | |
| fonts: [Uint8Array.from(fontBuff)], | |
| defaultFontFamily: { | |
| serifFamily: 'Source Han Sans SC', | |
| sansSerifFamily: 'Source Han Sans SC', | |
| cursiveFamily: 'Source Han Sans SC', | |
| fantasyFamily: 'Source Han Sans SC', | |
| monospaceFamily: 'Source Han Sans SC', | |
| } | |
| }); | |
| } | |
| this.canvas = require('@napi-rs/canvas'); | |
| this.emit('ready'); | |
| } | |
| () | |
| async renderSvgToPng(svgContent: string,) { | |
| return this.svg2png(svgContent, { backgroundColor: '#D3D3D3' }); | |
| } | |
| protected async _loadImage(input: string | Buffer) { | |
| let buff; | |
| let contentType; | |
| do { | |
| if (typeof input === 'string') { | |
| if (input.startsWith('data:')) { | |
| const firstComma = input.indexOf(','); | |
| const header = input.slice(0, firstComma); | |
| const data = input.slice(firstComma + 1); | |
| const encoding = header.split(';')[1]; | |
| contentType = header.split(';')[0].split(':')[1]; | |
| if (encoding?.startsWith('base64')) { | |
| buff = Buffer.from(data, 'base64'); | |
| } else { | |
| buff = Buffer.from(decodeURIComponent(data), 'utf-8'); | |
| } | |
| break; | |
| } | |
| if (input.startsWith('http')) { | |
| const r = await downloadFile(input); | |
| buff = Buffer.from(r.buff); | |
| contentType = r.contentType; | |
| break; | |
| } | |
| } | |
| if (Buffer.isBuffer(input)) { | |
| buff = input; | |
| const mime = await mimeOf(buff); | |
| contentType = `${mime.mediaType}/${mime.subType}`; | |
| break; | |
| } | |
| throw new ParamValidationError('Invalid input'); | |
| } while (false); | |
| if (!buff) { | |
| throw new ParamValidationError('Invalid input'); | |
| } | |
| if (contentType?.includes('svg')) { | |
| buff = await this.renderSvgToPng(buff.toString('utf-8')); | |
| } | |
| const img = await this.canvas.loadImage(buff); | |
| Reflect.set(img, 'contentType', contentType); | |
| return img; | |
| } | |
| async loadImage(uri: string | Buffer) { | |
| const t0 = Date.now(); | |
| try { | |
| const theImage = await this._loadImage(uri); | |
| const t1 = Date.now(); | |
| this.logger.debug(`Image loaded in ${t1 - t0}ms`); | |
| return theImage; | |
| } catch (err: any) { | |
| if (err?.message?.includes('Unsupported image type') || err?.message?.includes('unsupported')) { | |
| this.logger.warn(`Failed to load image ${uri.slice(0, 128)}`, { err }); | |
| throw new SubmittedDataMalformedError(`Unknown image format for ${uri.slice(0, 128)}`); | |
| } | |
| throw err; | |
| } | |
| } | |
| fitImageToSquareBox(image: canvas.Image | canvas.Canvas, size: number = 1024) { | |
| // this.logger.debug(`Fitting image(${ image.width }x${ image.height }) to ${ size } box`); | |
| // const t0 = Date.now(); | |
| if (image.width <= size && image.height <= size) { | |
| if (image instanceof this.canvas.Canvas) { | |
| return image; | |
| } | |
| const canvasInstance = this.canvas.createCanvas(image.width, image.height); | |
| const ctx = canvasInstance.getContext('2d'); | |
| ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvasInstance.width, canvasInstance.height); | |
| // this.logger.debug(`No need to resize, copied to canvas in ${ Date.now() - t0 } ms`); | |
| return canvasInstance; | |
| } | |
| const aspectRatio = image.width / image.height; | |
| const resizedWidth = Math.round(aspectRatio > 1 ? size : size * aspectRatio); | |
| const resizedHeight = Math.round(aspectRatio > 1 ? size / aspectRatio : size); | |
| const canvasInstance = this.canvas.createCanvas(resizedWidth, resizedHeight); | |
| const ctx = canvasInstance.getContext('2d'); | |
| ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, resizedWidth, resizedHeight); | |
| // this.logger.debug(`Resized to ${ resizedWidth }x${ resizedHeight } in ${ Date.now() - t0 } ms`); | |
| return canvasInstance; | |
| } | |
| corpImage(image: canvas.Image | canvas.Canvas, x: number, y: number, w: number, h: number) { | |
| // this.logger.debug(`Cropping image(${ image.width }x${ image.height }) to ${ w }x${ h } at ${ x },${ y } `); | |
| // const t0 = Date.now(); | |
| const canvasInstance = this.canvas.createCanvas(w, h); | |
| const ctx = canvasInstance.getContext('2d'); | |
| ctx.drawImage(image, x, y, w, h, 0, 0, w, h); | |
| // this.logger.debug(`Crop complete in ${ Date.now() - t0 } ms`); | |
| return canvasInstance; | |
| } | |
| canvasToDataUrl(canvas: canvas.Canvas, mimeType?: 'image/png' | 'image/jpeg') { | |
| // this.logger.debug(`Exporting canvas(${ canvas.width }x${ canvas.height })`); | |
| // const t0 = Date.now(); | |
| return canvas.toDataURLAsync((mimeType || 'image/png') as 'image/png'); | |
| } | |
| async canvasToBuffer(canvas: canvas.Canvas, mimeType?: 'image/png' | 'image/jpeg') { | |
| // this.logger.debug(`Exporting canvas(${ canvas.width }x${ canvas.height })`); | |
| // const t0 = Date.now(); | |
| return canvas.toBuffer((mimeType || 'image/png') as 'image/png'); | |
| } | |
| } | |
| const instance = container.resolve(CanvasService); | |
| export default instance; | |