diff --git a/examples/demo4.html b/examples/demo4.html index ed242e2..bd805c4 100644 --- a/examples/demo4.html +++ b/examples/demo4.html @@ -1,79 +1,72 @@ + + Nested transform tests + + + - body { - font-family: Arial; - } - - img.test { - width: 100px; - -webkit-filter: saturate(1600%); - /* Chrome, Safari, Opera */ - filter: saturate(1600%); - } - - - - - -
-
- - - - - \ No newline at end of file + +
+ +
+ + + + diff --git a/src/css/index.ts b/src/css/index.ts index 7604a1b..499c903 100644 --- a/src/css/index.ts +++ b/src/css/index.ts @@ -105,6 +105,7 @@ export class CSSParsedDeclaration { color: Color; display: ReturnType; filter: ReturnType; + filterOriginal: string | null; float: ReturnType; fontFamily: ReturnType; fontSize: LengthPercentage; @@ -171,6 +172,7 @@ export class CSSParsedDeclaration { this.color = parse(color, declaration.color); this.display = parse(display, declaration.display); this.filter = parse(filter, declaration.filter); + this.filterOriginal = declaration.filter; this.float = parse(float, declaration.cssFloat); this.fontFamily = parse(fontFamily, declaration.fontFamily); this.fontSize = parse(fontSize, declaration.fontSize); diff --git a/src/css/property-descriptors/filter.ts b/src/css/property-descriptors/filter.ts index 08eb312..699933d 100644 --- a/src/css/property-descriptors/filter.ts +++ b/src/css/property-descriptors/filter.ts @@ -2,15 +2,10 @@ import {IPropertyListDescriptor, PropertyDescriptorParsingType} from '../IProper import {CSSValue, isIdentWithValue, CSSFunction, isCSSFunction} from '../syntax/parser'; import {isLength, Length} from '../types/length'; -export interface Filter { - contrast: Length | null; - 'hue-rotate': Length | null; - grayscale: Length | null; - brightness: Length | null; - blur: Length | null; - invert: Length | null; - saturate: Length | null; - sepia: Length | null; +export type Filter = FilterItem[]; +export interface FilterItem { + name: string; + value: Length; } export const filter: IPropertyListDescriptor = { @@ -22,17 +17,7 @@ export const filter: IPropertyListDescriptor = { if (tokens.length === 1 && isIdentWithValue(tokens[0], 'none')) { return null; } - - const filter: Filter = { - contrast: null, - 'hue-rotate': null, - grayscale: null, - brightness: null, - blur: null, - invert: null, - saturate: null, - sepia: null - }; + const filter: Filter = []; let hasFilter: boolean = false; @@ -50,7 +35,7 @@ export const filter: IPropertyListDescriptor = { const value: CSSValue = token.values[index]; if (isLength(value)) { hasFilter = true; - filter[token.name] = value; + filter.push({name: token.name, value: value}); } } break; diff --git a/src/render/canvas/canvas-renderer.ts b/src/render/canvas/canvas-renderer.ts index a2dda99..34059a0 100644 --- a/src/render/canvas/canvas-renderer.ts +++ b/src/render/canvas/canvas-renderer.ts @@ -1,17 +1,5 @@ import {ElementPaint, parseStackingContexts, StackingContext} from '../stacking-context'; -import { - asString, - Color, - isTransparent, - RGBColor, - contrastRGB, - hueRotateRGB, - grayscaleRGB, - brightnessRGB, - invertRGB, - saturateRGB, - sepiaRGB -} from '../../css/types/color'; +import {asString, Color, isTransparent} from '../../css/types/color'; import {Logger} from '../../core/logger'; import {ElementContainer} from '../../dom/element-container'; import {BORDER_STYLE} from '../../css/property-descriptors/border-style'; @@ -50,8 +38,7 @@ import {TextareaElementContainer} from '../../dom/elements/textarea-element-cont import {SelectElementContainer} from '../../dom/elements/select-element-container'; import {IFrameElementContainer} from '../../dom/replaced-elements/iframe-element-container'; import {TextShadow} from '../../css/property-descriptors/text-shadow'; -import {Filter} from '../../css/property-descriptors/filter'; -import {stackBlurImage} from '../../css/types/functions/stack-blur'; +import {processImage, isSupportedFilter} from '../image-filter'; export type RenderConfigurations = RenderOptions & { backgroundColor: Color | null; @@ -262,8 +249,7 @@ export class CanvasRenderer { renderReplacedElement( container: ReplacedElementContainer, curves: BoundCurves, - image: HTMLImageElement | HTMLCanvasElement, - filter?: Filter | null | undefined + image: HTMLImageElement | HTMLCanvasElement ) { if (image && container.intrinsicWidth > 0 && container.intrinsicHeight > 0) { const box = contentBox(container); @@ -271,6 +257,9 @@ export class CanvasRenderer { this.path(path); this.ctx.save(); this.ctx.clip(); + if (isSupportedFilter(this.ctx) && container.styles.filterOriginal) { + this.ctx.filter = container.styles.filterOriginal; + } this.ctx.drawImage( image, 0, @@ -282,49 +271,6 @@ export class CanvasRenderer { box.width, box.height ); - if (filter) { - try { - let imageData = this.ctx.getImageData(box.left, box.top, box.width, box.height); - for (let _j = 0; _j < imageData.height; _j++) { - for (let _i = 0; _i < imageData.width; _i++) { - let index = _j * 4 * imageData.width + _i * 4; - let rgb: RGBColor = { - r: imageData.data[index], - g: imageData.data[index + 1], - b: imageData.data[index + 2] - }; - if (filter.contrast) { - rgb = contrastRGB(rgb, filter.contrast.number); - } - if (filter['hue-rotate']) { - rgb = hueRotateRGB(rgb, filter['hue-rotate'].number); - } - if (filter.grayscale) { - rgb = grayscaleRGB(rgb, filter['grayscale'].number, 'luma:BT709'); - } - if (filter.brightness) { - rgb = brightnessRGB(rgb, filter['brightness'].number); - } - if (filter.invert) { - rgb = invertRGB(rgb, filter['invert'].number); - } - if (filter.saturate) { - rgb = saturateRGB(rgb, filter.saturate.number); - } - if (filter.sepia) { - rgb = sepiaRGB(rgb, filter.sepia.number); - } - imageData.data[index] = rgb.r; - imageData.data[index + 1] = rgb.g; - imageData.data[index + 2] = rgb.b; - } - } - if (filter.blur) { - imageData = stackBlurImage(imageData, box.width, box.height, filter.blur.number * 2.2, 1); - } - this.ctx.putImageData(imageData, box.left, box.top); - } catch (error) {} - } this.ctx.restore(); } } @@ -341,7 +287,8 @@ export class CanvasRenderer { if (container instanceof ImageElementContainer) { try { const image = await this.options.cache.match(container.src); - this.renderReplacedElement(container, curves, image, styles.filter); + if (styles.filter && !isSupportedFilter(this.ctx)) await processImage(image, styles.filter); + this.renderReplacedElement(container, curves, image); } catch (e) { Logger.getInstance(this.options.id).error(`Error loading image ${container.src}`); } diff --git a/src/render/image-filter.ts b/src/render/image-filter.ts new file mode 100644 index 0000000..19feb72 --- /dev/null +++ b/src/render/image-filter.ts @@ -0,0 +1,99 @@ +import { + RGBColor, + contrastRGB, + hueRotateRGB, + grayscaleRGB, + brightnessRGB, + invertRGB, + saturateRGB, + sepiaRGB +} from '../css/types/color'; +import {Filter, FilterItem} from '../css/property-descriptors/filter'; +import {stackBlurImage} from '../css/types/functions/stack-blur'; + +export const processImage = (img: HTMLImageElement, filter: Filter) => { + return new Promise((resolve, reject) => { + if (!img || !('naturalWidth' in img)) { + return resolve(); + } + + const w = img['naturalWidth']; + const h = img['naturalHeight']; + + const canvas = document.createElement('canvas'); + canvas.style.width = w + 'px'; + canvas.style.height = h + 'px'; + canvas.width = w * 2; + canvas.height = h * 2; + const context = canvas.getContext('2d') as CanvasRenderingContext2D; + + context.clearRect(0, 0, w, h); + context.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, 0, 0, w, h); + + let imageData: ImageData = context.getImageData(0, 0, w, h); + + handlePerPixel(imageData, filter); + + let blurFilter = filter.find((item: FilterItem) => item.name === 'blur'); + + if (blurFilter) { + imageData = stackBlurImage(imageData, w, h, blurFilter.value.number * 2, 1); + } + context.putImageData(imageData, 0, 0); + img.crossOrigin = 'anonymous'; + img.src = canvas.toDataURL(); + img.onload = () => resolve(img); + img.onerror = reject; + + if (img.complete === true) { + // Inline XML images may fail to parse, throwing an Error later on + setTimeout(() => resolve(img), 500); + } + }); +}; + +function handlePerPixel(imageData: ImageData, filter: Filter) { + for (let _j = 0; _j < imageData.height; _j++) { + for (let _i = 0; _i < imageData.width; _i++) { + let index = _j * 4 * imageData.width + _i * 4; + let rgb: RGBColor = { + r: imageData.data[index], + g: imageData.data[index + 1], + b: imageData.data[index + 2] + }; + filter.forEach((item: FilterItem) => { + switch (item.name) { + case 'contrast': + rgb = contrastRGB(rgb, item.value.number); + break; + case 'hue-rotate': + rgb = hueRotateRGB(rgb, item.value.number); + break; + case 'grayscale': + rgb = grayscaleRGB(rgb, item.value.number); + break; + case 'brightness': + rgb = brightnessRGB(rgb, item.value.number); + break; + case 'invert': + rgb = invertRGB(rgb, item.value.number); + break; + case 'saturate': + rgb = saturateRGB(rgb, item.value.number); + break; + case 'sepia': + rgb = sepiaRGB(rgb, item.value.number); + break; + default: + break; + } + }); + + imageData.data[index] = rgb.r; + imageData.data[index + 1] = rgb.g; + imageData.data[index + 2] = rgb.b; + } + } +} + +export const isSupportedFilter = (ctx: CanvasRenderingContext2D) => 'filter' in ctx;