Skip to content

Commit

Permalink
see pr: niklasvh#2955
Browse files Browse the repository at this point in the history
edited canvas-renderer to accommodate other pr for object fit (b23fc4d)
+ removed comment linking to firefox issue as it is now resolved
  • Loading branch information
nangelina committed Feb 11, 2024
1 parent 081fb70 commit 5956f39
Show file tree
Hide file tree
Showing 10 changed files with 151 additions and 84 deletions.
7 changes: 3 additions & 4 deletions src/core/__mocks__/context.ts
Expand Up @@ -9,10 +9,9 @@ export class Context {

constructor() {
this.cache = {
addImage: jest.fn().mockImplementation((src: string): Promise<void> => {
const result = Promise.resolve();
this._cache[src] = result;
return result;
addImage: jest.fn().mockImplementation((src: string): boolean => {
this._cache[src] = Promise.resolve();
return true;
})
};
}
Expand Down
60 changes: 35 additions & 25 deletions src/core/__tests__/cache-storage.ts
Expand Up @@ -125,96 +125,96 @@ describe('cache-storage', () => {
xhr.splice(0, xhr.length);
images.splice(0, images.length);
});
it('addImage adds images to cache', async () => {
it('addImage adds images to cache', () => {
const {cache} = createMockContext('http://example.com', {proxy: null});
await cache.addImage('http://example.com/test.jpg');
await cache.addImage('http://example.com/test2.jpg');
cache.addImage('http://example.com/test.jpg');
cache.addImage('http://example.com/test2.jpg');

deepStrictEqual(images.length, 2);
deepStrictEqual(images[0].src, 'http://example.com/test.jpg');
deepStrictEqual(images[1].src, 'http://example.com/test2.jpg');
});

it('addImage should not add duplicate entries', async () => {
it('addImage should not add duplicate entries', () => {
const {cache} = createMockContext('http://example.com');
await cache.addImage('http://example.com/test.jpg');
await cache.addImage('http://example.com/test.jpg');
cache.addImage('http://example.com/test.jpg');
cache.addImage('http://example.com/test.jpg');

deepStrictEqual(images.length, 1);
deepStrictEqual(images[0].src, 'http://example.com/test.jpg');
});

describe('svg', () => {
it('should add svg images correctly', async () => {
it('should add svg images correctly', () => {
const {cache} = createMockContext('http://example.com');
await cache.addImage('http://example.com/test.svg');
await cache.addImage('http://example.com/test2.svg');
cache.addImage('http://example.com/test.svg');
cache.addImage('http://example.com/test2.svg');

deepStrictEqual(images.length, 2);
deepStrictEqual(images[0].src, 'http://example.com/test.svg');
deepStrictEqual(images[1].src, 'http://example.com/test2.svg');
});

it('should omit svg images if not supported', async () => {
it('should omit svg images if not supported', () => {
setFeatures({SUPPORT_SVG_DRAWING: false});
const {cache} = createMockContext('http://example.com');
await cache.addImage('http://example.com/test.svg');
await cache.addImage('http://example.com/test2.svg');
cache.addImage('http://example.com/test.svg');
cache.addImage('http://example.com/test2.svg');

deepStrictEqual(images.length, 0);
});
});

describe('cross-origin', () => {
it('addImage should not add images it cannot load/render', async () => {
it('addImage should not add images it cannot load/render', () => {
const {cache} = createMockContext('http://example.com', {
proxy: undefined
});
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
cache.addImage('http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images.length, 0);
});

it('addImage should add images if tainting enabled', async () => {
it('addImage should add images if tainting enabled', () => {
const {cache} = createMockContext('http://example.com', {
allowTaint: true,
proxy: undefined
});
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
cache.addImage('http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images.length, 1);
deepStrictEqual(images[0].src, 'http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images[0].crossOrigin, undefined);
});

it('addImage should add images if cors enabled', async () => {
it('addImage should add images if cors enabled', () => {
const {cache} = createMockContext('http://example.com', {useCORS: true});
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
cache.addImage('http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images.length, 1);
deepStrictEqual(images[0].src, 'http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images[0].crossOrigin, 'anonymous');
});

it('addImage should not add images if cors enabled but not supported', async () => {
it('addImage should not add images if cors enabled but not supported', () => {
setFeatures({SUPPORT_CORS_IMAGES: false});

const {cache} = createMockContext('http://example.com', {
useCORS: true,
proxy: undefined
});
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
cache.addImage('http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images.length, 0);
});

it('addImage should not add images to proxy if cors enabled', async () => {
it('addImage should not add images to proxy if cors enabled', () => {
const {cache} = createMockContext('http://example.com', {useCORS: true});
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
cache.addImage('http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images.length, 1);
deepStrictEqual(images[0].src, 'http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(images[0].crossOrigin, 'anonymous');
});

it('addImage should use proxy ', async () => {
const {cache} = createMockContext('http://example.com');
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
cache.addImage('http://html2canvas.hertzen.com/test.jpg');
deepStrictEqual(xhr.length, 1);
deepStrictEqual(
xhr[0].url,
Expand All @@ -230,7 +230,7 @@ describe('cache-storage', () => {
const {cache} = createMockContext('http://example.com', {
imageTimeout: 10
});
await cache.addImage('http://html2canvas.hertzen.com/test.jpg');
cache.addImage('http://html2canvas.hertzen.com/test.jpg');

deepStrictEqual(xhr.length, 1);
deepStrictEqual(
Expand All @@ -250,7 +250,7 @@ describe('cache-storage', () => {

it('match should return cache entry', async () => {
const {cache} = createMockContext('http://example.com');
await cache.addImage('http://example.com/test.jpg');
cache.addImage('http://example.com/test.jpg');

if (images[0].onload) {
images[0].onload();
Expand All @@ -270,4 +270,14 @@ describe('cache-storage', () => {
fail('Expected result to timeout');
} catch (e) {}
});

it('addImage should add an inlined image', async () => {
const {cache} = createMockContext('http://example.com', {imageTimeout: 10});
const inlinedImg = `data:image/gif;base64,R0lGODlhEAAOALMAAOazToeHh0tLS/7LZv/0jvb29t/f3//Ub/
/ge8WSLf/rhf/3kdbW1mxsbP//mf///yH5BAAAAAAALAAAAAAQAA4AAARe8L1Ekyky67QZ1hLnjM5UUde0ECwLJoExKcpp
V0aCcGCmTIHEIUEqjgaORCMxIC6e0CcguWw6aFjsVMkkIr7g77ZKPJjPZqIyd7sJAgVGoEGv2xsBxqNgYPj/gAwXEQA7`;
cache.addImage(inlinedImg);

await cache.match(inlinedImg);
});
});
17 changes: 7 additions & 10 deletions src/core/cache-storage.ts
Expand Up @@ -39,20 +39,15 @@ export class Cache {

constructor(private readonly context: Context, private readonly _options: ResourceOptions) {}

addImage(src: string): Promise<void> {
const result = Promise.resolve();
if (this.has(src)) {
return result;
}

addImage(src: string): boolean {
if (this.has(src)) return true;
if (isBlobImage(src) || isRenderable(src)) {
(this._cache[src] = this.loadImage(src)).catch(() => {
// prevent unhandled rejection
});
return result;
return true;
}

return result;
return false;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -98,7 +93,9 @@ export class Cache {
img.crossOrigin = 'anonymous';
}
img.src = src;
if (img.complete === true) {
if (/^data:/.test(src)) {
resolve(img);
} else if (img.complete === true) {
// Inline XML images may fail to parse, throwing an Error later on
setTimeout(() => resolve(img), 500);
}
Expand Down
34 changes: 29 additions & 5 deletions src/core/features.ts
@@ -1,4 +1,5 @@
import {fromCodePoint, toCodePoints} from 'css-line-break';
import {isSVGForeignObjectElement} from '../dom/node-parser';

const testRangeBounds = (document: Document) => {
const TEST_HEIGHT = 123;
Expand Down Expand Up @@ -156,15 +157,38 @@ export const createForeignObjectSVG = (
return svg;
};

export const loadSerializedSVG = (svg: Node): Promise<HTMLImageElement> => {
return new Promise((resolve, reject) => {
export const serializeSvg = (svg: SVGSVGElement | SVGForeignObjectElement, encoding = ''): string => {
const svgPrefix = 'data:image/svg+xml';
const selializedSvg = new XMLSerializer().serializeToString(svg);
const encodedSvg = encoding === 'base64' ? btoa(selializedSvg) : encodeURIComponent(selializedSvg);
return `${svgPrefix}${encoding && `;${encoding}`},${encodedSvg}`;
};

const INLINE_BASE64 = /^data:image\/.*;base64,/i;
export const deserializeSvg = (svg: string): SVGSVGElement | SVGForeignObjectElement => {
const encodedSvg = INLINE_BASE64.test(svg) ? atob(svg) : decodeURIComponent(svg);
const domParser = new DOMParser();
const document = domParser.parseFromString(encodedSvg, 'image/svg+xml');
const parserError = document.querySelector('parsererror');
if (parserError) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Expected 0-1 arguments, but got 2.
throw new Error('Deserialisation failed', {cause: parserError});
}
const {documentElement} = document;
const firstSvgChild = documentElement.firstElementChild;
return firstSvgChild && isSVGForeignObjectElement(firstSvgChild)
? (documentElement as unknown as SVGForeignObjectElement)
: (documentElement as unknown as SVGSVGElement);
};

export const loadSerializedSVG = (svg: SVGSVGElement | SVGForeignObjectElement): Promise<HTMLImageElement> =>
new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;

img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}`;
img.src = serializeSvg(svg, 'charset=utf-8');
});
};

export const FEATURES = {
get SUPPORT_RANGE_BOUNDS(): boolean {
Expand Down
1 change: 1 addition & 0 deletions src/dom/node-parser.ts
Expand Up @@ -122,6 +122,7 @@ export const isOLElement = (node: Element): node is HTMLOListElement => node.tag
export const isInputElement = (node: Element): node is HTMLInputElement => node.tagName === 'INPUT';
export const isHTMLElement = (node: Element): node is HTMLHtmlElement => node.tagName === 'HTML';
export const isSVGElement = (node: Element): node is SVGSVGElement => node.tagName === 'svg';
export const isSVGForeignObjectElement = (node: Element): node is SVGSVGElement => node.tagName === 'foreignObject';
export const isBodyElement = (node: Element): node is HTMLBodyElement => node.tagName === 'BODY';
export const isCanvasElement = (node: Element): node is HTMLCanvasElement => node.tagName === 'CANVAS';
export const isVideoElement = (node: Element): node is HTMLVideoElement => node.tagName === 'VIDEO';
Expand Down
4 changes: 2 additions & 2 deletions src/dom/replaced-elements/iframe-element-container.ts
Expand Up @@ -13,8 +13,8 @@ export class IFrameElementContainer extends ElementContainer {
constructor(context: Context, iframe: HTMLIFrameElement) {
super(context, iframe);
this.src = iframe.src;
this.width = parseInt(iframe.width, 10) || 0;
this.height = parseInt(iframe.height, 10) || 0;
this.width = parseInt(iframe.width, 10) || iframe.offsetWidth || 0;
this.height = parseInt(iframe.height, 10) || iframe.offsetHeight || 0;
this.backgroundColor = this.styles.backgroundColor;
try {
if (
Expand Down
40 changes: 37 additions & 3 deletions src/dom/replaced-elements/image-element-container.ts
@@ -1,16 +1,50 @@
import {ElementContainer} from '../element-container';
import {Context} from '../../core/context';
import {serializeSvg, deserializeSvg} from '../../core/features';

export class ImageElementContainer extends ElementContainer {
src: string;
intrinsicWidth: number;
intrinsicHeight: number;
intrinsicWidth: number = 0;
intrinsicHeight: number = 0;
isSVG: boolean;

private static SVG = /\.svg(?:\?.*)?$/i;
private static INLINED_SVG = /^data:image\/svg\+xml/i;
private static IS_FIRE_FOX = /firefox/i.test(navigator?.userAgent);

constructor(context: Context, img: HTMLImageElement) {
super(context, img);
this.src = img.currentSrc || img.src;
this.isSVG = this.isSvg() || this.isInlinedSvg();
this.context.cache.addImage(this.src);
}

private isInlinedSvg = () => ImageElementContainer.INLINED_SVG.test(this.src);
private isSvg = () => ImageElementContainer.SVG.test(this.src);

public setup(img: HTMLImageElement) {
if (this.isSvg()) return;

if (this.isInlinedSvg()) {
const [, inlinedSvg] = this.src.split(',');
const svgElement = deserializeSvg(inlinedSvg);
const {
width: {baseVal: widthBaseVal},
height: {baseVal: heightBaseVal}
} = svgElement;

if (ImageElementContainer.IS_FIRE_FOX) {
widthBaseVal.valueAsString = widthBaseVal.value.toString();
heightBaseVal.valueAsString = heightBaseVal.value.toString();
img.src = serializeSvg(svgElement, 'base64');
}

this.intrinsicWidth = widthBaseVal.value;
this.intrinsicHeight = heightBaseVal.value;
return;
}

this.intrinsicWidth = img.naturalWidth;
this.intrinsicHeight = img.naturalHeight;
this.context.cache.addImage(this.src);
}
}
5 changes: 3 additions & 2 deletions src/dom/replaced-elements/svg-element-container.ts
@@ -1,6 +1,7 @@
import {ElementContainer} from '../element-container';
import {parseBounds} from '../../css/layout/bounds';
import {Context} from '../../core/context';
import {serializeSvg} from '../../core/features';

export class SVGElementContainer extends ElementContainer {
svg: string;
Expand All @@ -9,7 +10,7 @@ export class SVGElementContainer extends ElementContainer {

constructor(context: Context, img: SVGSVGElement) {
super(context, img);
const s = new XMLSerializer();

const bounds = parseBounds(context, img);
const originPosition: string = img.style.position;
img.setAttribute('width', `${bounds.width}px`);
Expand All @@ -20,7 +21,7 @@ export class SVGElementContainer extends ElementContainer {
// so, it is necessary to eliminate positioning before serialization.
img.style.position = 'initial';

this.svg = `data:image/svg+xml,${encodeURIComponent(s.serializeToString(img))}`;
this.svg = serializeSvg(img);

// reset position
img.style.position = originPosition;
Expand Down

0 comments on commit 5956f39

Please sign in to comment.