UNPKG

fabric

Version:

Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.

548 lines (509 loc) 16 kB
import { BaseFilter } from './BaseFilter'; import type { T2DPipelineState, TWebGLPipelineState, TWebGLUniformLocationMap, } from './typedefs'; import { isWebGLPipelineState } from './utils'; import { classRegistry } from '../ClassRegistry'; import { createCanvasElement } from '../util/misc/dom'; import type { XY } from '../Point'; export type TResizeType = 'bilinear' | 'hermite' | 'sliceHack' | 'lanczos'; export type ResizeOwnProps = { resizeType: TResizeType; scaleX: number; scaleY: number; lanczosLobes: number; }; export const resizeDefaultValues: ResizeOwnProps = { resizeType: 'hermite', scaleX: 1, scaleY: 1, lanczosLobes: 3, }; type ResizeDuring2DResize = Resize & { rcpScaleX: number; rcpScaleY: number; }; type ResizeDuringWEBGLResize = Resize & { rcpScaleX: number; rcpScaleY: number; horizontal: boolean; width: number; height: number; taps: number[]; tempScale: number; dH: number; dW: number; }; /** * Resize image filter class * @example * const filter = new Resize(); * object.filters.push(filter); * object.applyFilters(canvas.renderAll.bind(canvas)); */ export class Resize extends BaseFilter<'Resize', ResizeOwnProps> { /** * Resize type * for webgl resizeType is just lanczos, for canvas2d can be: * bilinear, hermite, sliceHack, lanczos. * @default */ declare resizeType: ResizeOwnProps['resizeType']; /** * Scale factor for resizing, x axis * @param {Number} scaleX * @default */ declare scaleX: ResizeOwnProps['scaleX']; /** * Scale factor for resizing, y axis * @param {Number} scaleY * @default */ declare scaleY: ResizeOwnProps['scaleY']; /** * LanczosLobes parameter for lanczos filter, valid for resizeType lanczos * @param {Number} lanczosLobes * @default */ declare lanczosLobes: ResizeOwnProps['lanczosLobes']; static type = 'Resize'; static defaults = resizeDefaultValues; static uniformLocations = ['uDelta', 'uTaps']; /** * Send data from this filter to its shader program's uniforms. * * @param {WebGLRenderingContext} gl The GL canvas context used to compile this filter's shader. * @param {Object} uniformLocations A map of string uniform names to WebGLUniformLocation objects */ sendUniformData( this: ResizeDuringWEBGLResize, gl: WebGLRenderingContext, uniformLocations: TWebGLUniformLocationMap, ) { gl.uniform2fv( uniformLocations.uDelta, this.horizontal ? [1 / this.width, 0] : [0, 1 / this.height], ); gl.uniform1fv(uniformLocations.uTaps, this.taps); } getFilterWindow(this: ResizeDuringWEBGLResize) { const scale = this.tempScale; return Math.ceil(this.lanczosLobes / scale); } getCacheKey(this: ResizeDuringWEBGLResize): string { const filterWindow = this.getFilterWindow(); return `${this.type}_${filterWindow}`; } getFragmentSource(this: ResizeDuringWEBGLResize): string { const filterWindow = this.getFilterWindow(); return this.generateShader(filterWindow); } getTaps(this: ResizeDuringWEBGLResize) { const lobeFunction = this.lanczosCreate(this.lanczosLobes), scale = this.tempScale, filterWindow = this.getFilterWindow(), taps = new Array(filterWindow); for (let i = 1; i <= filterWindow; i++) { taps[i - 1] = lobeFunction(i * scale); } return taps; } /** * Generate vertex and shader sources from the necessary steps numbers * @param {Number} filterWindow */ generateShader(filterWindow: number) { const offsets = new Array(filterWindow); for (let i = 1; i <= filterWindow; i++) { offsets[i - 1] = `${i}.0 * uDelta`; } return ` precision highp float; uniform sampler2D uTexture; uniform vec2 uDelta; varying vec2 vTexCoord; uniform float uTaps[${filterWindow}]; void main() { vec4 color = texture2D(uTexture, vTexCoord); float sum = 1.0; ${offsets .map( (offset, i) => ` color += texture2D(uTexture, vTexCoord + ${offset}) * uTaps[${i}] + texture2D(uTexture, vTexCoord - ${offset}) * uTaps[${i}]; sum += 2.0 * uTaps[${i}]; `, ) .join('\n')} gl_FragColor = color / sum; } `; } applyToForWebgl(this: ResizeDuringWEBGLResize, options: TWebGLPipelineState) { options.passes++; this.width = options.sourceWidth; this.horizontal = true; this.dW = Math.round(this.width * this.scaleX); this.dH = options.sourceHeight; this.tempScale = this.dW / this.width; this.taps = this.getTaps(); options.destinationWidth = this.dW; super.applyTo(options); options.sourceWidth = options.destinationWidth; this.height = options.sourceHeight; this.horizontal = false; this.dH = Math.round(this.height * this.scaleY); this.tempScale = this.dH / this.height; this.taps = this.getTaps(); options.destinationHeight = this.dH; super.applyTo(options); options.sourceHeight = options.destinationHeight; } /** * Apply the resize filter to the image * Determines whether to use WebGL or Canvas2D based on the options.webgl flag. * * @param {Object} options * @param {Number} options.passes The number of filters remaining to be executed * @param {Boolean} options.webgl Whether to use webgl to render the filter. * @param {WebGLTexture} options.sourceTexture The texture setup as the source to be filtered. * @param {WebGLTexture} options.targetTexture The texture where filtered output should be drawn. * @param {WebGLRenderingContext} options.context The GL context used for rendering. * @param {Object} options.programCache A map of compiled shader programs, keyed by filter type. */ applyTo(options: TWebGLPipelineState | T2DPipelineState) { if (isWebGLPipelineState(options)) { (this as unknown as ResizeDuringWEBGLResize).applyToForWebgl(options); } else { (this as unknown as ResizeDuring2DResize).applyTo2d(options); } } isNeutralState() { return this.scaleX === 1 && this.scaleY === 1; } lanczosCreate(lobes: number) { return (x: number) => { if (x >= lobes || x <= -lobes) { return 0.0; } if (x < 1.1920929e-7 && x > -1.1920929e-7) { return 1.0; } x *= Math.PI; const xx = x / lobes; return ((Math.sin(x) / x) * Math.sin(xx)) / xx; }; } applyTo2d(this: ResizeDuring2DResize, options: T2DPipelineState) { const imageData = options.imageData, scaleX = this.scaleX, scaleY = this.scaleY; this.rcpScaleX = 1 / scaleX; this.rcpScaleY = 1 / scaleY; const oW = imageData.width; const oH = imageData.height; const dW = Math.round(oW * scaleX); const dH = Math.round(oH * scaleY); let newData: ImageData; if (this.resizeType === 'sliceHack') { newData = this.sliceByTwo(options, oW, oH, dW, dH); } else if (this.resizeType === 'hermite') { newData = this.hermiteFastResize(options, oW, oH, dW, dH); } else if (this.resizeType === 'bilinear') { newData = this.bilinearFiltering(options, oW, oH, dW, dH); } else if (this.resizeType === 'lanczos') { newData = this.lanczosResize(options, oW, oH, dW, dH); } else { // this should never trigger, is here just for safety net. newData = new ImageData(dW, dH); } options.imageData = newData; } /** * Filter sliceByTwo * @param {Object} canvasEl Canvas element to apply filter to * @param {Number} oW Original Width * @param {Number} oH Original Height * @param {Number} dW Destination Width * @param {Number} dH Destination Height * @returns {ImageData} */ sliceByTwo( options: T2DPipelineState, oW: number, oH: number, dW: number, dH: number, ) { const imageData = options.imageData; const mult = 0.5; let doneW = false; let doneH = false; let stepW = oW * mult; let stepH = oH * mult; const resources = options.filterBackend.resources; let sX = 0; let sY = 0; const dX = oW; let dY = 0; if (!resources.sliceByTwo) { resources.sliceByTwo = createCanvasElement(); } const tmpCanvas = resources.sliceByTwo; if (tmpCanvas.width < oW * 1.5 || tmpCanvas.height < oH) { tmpCanvas.width = oW * 1.5; tmpCanvas.height = oH; } const ctx = tmpCanvas.getContext('2d')!; ctx.clearRect(0, 0, oW * 1.5, oH); ctx.putImageData(imageData, 0, 0); dW = Math.floor(dW); dH = Math.floor(dH); while (!doneW || !doneH) { oW = stepW; oH = stepH; if (dW < Math.floor(stepW * mult)) { stepW = Math.floor(stepW * mult); } else { stepW = dW; doneW = true; } if (dH < Math.floor(stepH * mult)) { stepH = Math.floor(stepH * mult); } else { stepH = dH; doneH = true; } ctx.drawImage(tmpCanvas, sX, sY, oW, oH, dX, dY, stepW, stepH); sX = dX; sY = dY; dY += stepH; } return ctx.getImageData(sX, sY, dW, dH); } /** * Filter lanczosResize * @param {Object} canvasEl Canvas element to apply filter to * @param {Number} oW Original Width * @param {Number} oH Original Height * @param {Number} dW Destination Width * @param {Number} dH Destination Height * @returns {ImageData} */ lanczosResize( this: ResizeDuring2DResize, options: T2DPipelineState, oW: number, oH: number, dW: number, dH: number, ): ImageData { function process(u: number): ImageData { let v, i, weight, idx, a, red, green, blue, alpha, fX, fY; center.x = (u + 0.5) * ratioX; icenter.x = Math.floor(center.x); for (v = 0; v < dH; v++) { center.y = (v + 0.5) * ratioY; icenter.y = Math.floor(center.y); a = 0; red = 0; green = 0; blue = 0; alpha = 0; for (i = icenter.x - range2X; i <= icenter.x + range2X; i++) { if (i < 0 || i >= oW) { continue; } fX = Math.floor(1000 * Math.abs(i - center.x)); if (!cacheLanc[fX]) { cacheLanc[fX] = {}; } for (let j = icenter.y - range2Y; j <= icenter.y + range2Y; j++) { if (j < 0 || j >= oH) { continue; } fY = Math.floor(1000 * Math.abs(j - center.y)); if (!cacheLanc[fX][fY]) { cacheLanc[fX][fY] = lanczos( Math.sqrt( Math.pow(fX * rcpRatioX, 2) + Math.pow(fY * rcpRatioY, 2), ) / 1000, ); } weight = cacheLanc[fX][fY]; if (weight > 0) { idx = (j * oW + i) * 4; a += weight; red += weight * srcData[idx]; green += weight * srcData[idx + 1]; blue += weight * srcData[idx + 2]; alpha += weight * srcData[idx + 3]; } } } idx = (v * dW + u) * 4; destData[idx] = red / a; destData[idx + 1] = green / a; destData[idx + 2] = blue / a; destData[idx + 3] = alpha / a; } if (++u < dW) { return process(u); } else { return destImg; } } const srcData = options.imageData.data, destImg = options.ctx.createImageData(dW, dH), destData = destImg.data, lanczos = this.lanczosCreate(this.lanczosLobes), ratioX = this.rcpScaleX, ratioY = this.rcpScaleY, rcpRatioX = 2 / this.rcpScaleX, rcpRatioY = 2 / this.rcpScaleY, range2X = Math.ceil((ratioX * this.lanczosLobes) / 2), range2Y = Math.ceil((ratioY * this.lanczosLobes) / 2), cacheLanc: Record<number, Record<number, number>> = {}, center: XY = { x: 0, y: 0 }, icenter: XY = { x: 0, y: 0 }; return process(0); } /** * bilinearFiltering * @param {Object} canvasEl Canvas element to apply filter to * @param {Number} oW Original Width * @param {Number} oH Original Height * @param {Number} dW Destination Width * @param {Number} dH Destination Height * @returns {ImageData} */ bilinearFiltering( this: ResizeDuring2DResize, options: T2DPipelineState, oW: number, oH: number, dW: number, dH: number, ) { let a; let b; let c; let d; let x; let y; let i; let j; let xDiff; let yDiff; let chnl; let color; let offset = 0; let origPix; const ratioX = this.rcpScaleX; const ratioY = this.rcpScaleY; const w4 = 4 * (oW - 1); const img = options.imageData; const pixels = img.data; const destImage = options.ctx.createImageData(dW, dH); const destPixels = destImage.data; for (i = 0; i < dH; i++) { for (j = 0; j < dW; j++) { x = Math.floor(ratioX * j); y = Math.floor(ratioY * i); xDiff = ratioX * j - x; yDiff = ratioY * i - y; origPix = 4 * (y * oW + x); for (chnl = 0; chnl < 4; chnl++) { a = pixels[origPix + chnl]; b = pixels[origPix + 4 + chnl]; c = pixels[origPix + w4 + chnl]; d = pixels[origPix + w4 + 4 + chnl]; color = a * (1 - xDiff) * (1 - yDiff) + b * xDiff * (1 - yDiff) + c * yDiff * (1 - xDiff) + d * xDiff * yDiff; destPixels[offset++] = color; } } } return destImage; } /** * hermiteFastResize * @param {Object} canvasEl Canvas element to apply filter to * @param {Number} oW Original Width * @param {Number} oH Original Height * @param {Number} dW Destination Width * @param {Number} dH Destination Height * @returns {ImageData} */ hermiteFastResize( this: ResizeDuring2DResize, options: T2DPipelineState, oW: number, oH: number, dW: number, dH: number, ) { const ratioW = this.rcpScaleX, ratioH = this.rcpScaleY, ratioWHalf = Math.ceil(ratioW / 2), ratioHHalf = Math.ceil(ratioH / 2), img = options.imageData, data = img.data, img2 = options.ctx.createImageData(dW, dH), data2 = img2.data; for (let j = 0; j < dH; j++) { for (let i = 0; i < dW; i++) { const x2 = (i + j * dW) * 4; let weight = 0; let weights = 0; let weightsAlpha = 0; let gxR = 0; let gxG = 0; let gxB = 0; let gxA = 0; const centerY = (j + 0.5) * ratioH; for (let yy = Math.floor(j * ratioH); yy < (j + 1) * ratioH; yy++) { const dy = Math.abs(centerY - (yy + 0.5)) / ratioHHalf, centerX = (i + 0.5) * ratioW, w0 = dy * dy; for (let xx = Math.floor(i * ratioW); xx < (i + 1) * ratioW; xx++) { let dx = Math.abs(centerX - (xx + 0.5)) / ratioWHalf; const w = Math.sqrt(w0 + dx * dx); /* eslint-disable max-depth */ if (w > 1 && w < -1) { continue; } //hermite filter weight = 2 * w * w * w - 3 * w * w + 1; if (weight > 0) { dx = 4 * (xx + yy * oW); //alpha gxA += weight * data[dx + 3]; weightsAlpha += weight; //colors if (data[dx + 3] < 255) { weight = (weight * data[dx + 3]) / 250; } gxR += weight * data[dx]; gxG += weight * data[dx + 1]; gxB += weight * data[dx + 2]; weights += weight; } /* eslint-enable max-depth */ } } data2[x2] = gxR / weights; data2[x2 + 1] = gxG / weights; data2[x2 + 2] = gxB / weights; data2[x2 + 3] = gxA / weightsAlpha; } } return img2; } } classRegistry.setClass(Resize);