Permalink
Browse files

Inline fonts for ForeignObjectRenderer

  • Loading branch information...
1 parent f16d581 commit 9445b0b598f939d46a61c804cd7a35f29ad28f6b @niklasvh committed Oct 18, 2017
View
@@ -30,14 +30,14 @@
"base64-arraybuffer": "0.1.5",
"body-parser": "1.17.2",
"chai": "4.1.1",
- "chromeless": "^1.2.0",
+ "chromeless": "1.2.0",
"cors": "2.8.4",
"eslint": "4.2.0",
"eslint-plugin-flowtype": "2.35.0",
"eslint-plugin-prettier": "2.1.2",
"express": "4.15.4",
"filenamify-url": "1.0.0",
- "flow-bin": "0.50.0",
+ "flow-bin": "0.56.0",
"glob": "7.1.2",
"html2canvas-proxy": "1.0.0",
"jquery": "3.2.1",
View
@@ -3,11 +3,10 @@
import type {Bounds} from './Bounds';
import type {Options} from './index';
import type Logger from './Logger';
-import type {ImageElement} from './ImageLoader';
import {parseBounds} from './Bounds';
import {Proxy} from './Proxy';
-import ImageLoader from './ImageLoader';
+import ResourceLoader from './ResourceLoader';
import {copyCSSStyles} from './Util';
import {parseBackgroundImage} from './parsing/background';
import CanvasRenderer from './renderer/CanvasRenderer';
@@ -17,7 +16,7 @@ export class DocumentCloner {
referenceElement: HTMLElement;
clonedReferenceElement: HTMLElement;
documentElement: HTMLElement;
- imageLoader: ImageLoader<*>;
+ resourceLoader: ResourceLoader;
logger: Logger;
options: Options;
inlineImages: boolean;
@@ -38,7 +37,7 @@ export class DocumentCloner {
this.logger = logger;
this.options = options;
this.renderer = renderer;
- this.imageLoader = new ImageLoader(options, logger, window);
+ this.resourceLoader = new ResourceLoader(options, logger, window);
// $FlowFixMe
this.documentElement = this.cloneNode(element.ownerDocument.documentElement);
}
@@ -49,9 +48,14 @@ export class DocumentCloner {
Promise.all(
parseBackgroundImage(style.backgroundImage).map(backgroundImage => {
if (backgroundImage.method === 'url') {
- return this.imageLoader
+ return this.resourceLoader
.inlineImage(backgroundImage.args[0])
- .then(img => (img ? `url("${img.src}")` : 'none'))
+ .then(
+ img =>
+ img && typeof img.src === 'string'
+ ? `url("${img.src}")`
+ : 'none'
+ )
.catch(e => {
if (__DEV__) {
this.logger.log(`Unable to load image`, e);
@@ -73,7 +77,7 @@ export class DocumentCloner {
});
if (node instanceof HTMLImageElement) {
- this.imageLoader
+ this.resourceLoader
.inlineImage(node.src)
.then(img => {
if (img && node instanceof HTMLImageElement && node.parentNode) {
@@ -91,6 +95,56 @@ export class DocumentCloner {
}
}
+ inlineFonts(document: Document): Promise<void> {
+ return Promise.all(
+ Array.from(document.styleSheets).map(sheet => {
+ if (sheet.href) {
+ return fetch(sheet.href)
+ .then(res => res.text())
+ .then(text => createStyleSheetFontsFromText(text, sheet.href))
+ .catch(e => {
+ if (__DEV__) {
+ this.logger.log(`Unable to load stylesheet`, e);
+ }
+ return [];
+ });
+ }
+ return getSheetFonts(sheet, document);
+ })
+ )
+ .then(fonts => fonts.reduce((acc, font) => acc.concat(font), []))
+ .then(fonts =>
+ Promise.all(
+ fonts.map(font =>
+ fetch(font.formats[0].src)
+ .then(response => response.blob())
+ .then(
+ blob =>
+ new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onerror = reject;
+ reader.onload = () => {
+ // $FlowFixMe
+ const result: string = reader.result;
+ resolve(result);
+ };
+ reader.readAsDataURL(blob);
+ })
+ )
+ .then(dataUri => {
+ font.fontFace.setProperty('src', `url("${dataUri}")`);
+ return `@font-face {${font.fontFace.cssText} `;
+ })
+ )
+ )
+ )
+ .then(fontCss => {
+ const style = document.createElement('style');
+ style.textContent = fontCss.join('\n');
+ this.documentElement.appendChild(style);
+ });
+ }
+
createElementClone(node: Node) {
if (this.copyStyles && node instanceof HTMLCanvasElement) {
const img = node.ownerDocument.createElement('img');
@@ -111,7 +165,7 @@ export class DocumentCloner {
const {width, height} = parseBounds(node, 0, 0);
- this.imageLoader.cache[iframeKey] = getIframeDocumentElement(node, this.options)
+ this.resourceLoader.cache[iframeKey] = getIframeDocumentElement(node, this.options)
.then(documentElement => {
return this.renderer(
documentElement,
@@ -211,6 +265,67 @@ export class DocumentCloner {
}
}
+type Font = {
+ src: string,
+ format: string
+};
+
+type FontFamily = {
+ formats: Array<Font>,
+ fontFace: CSSStyleDeclaration
+};
+
+const getSheetFonts = (sheet: StyleSheet, document: Document): Array<FontFamily> => {
+ // $FlowFixMe
+ return (sheet.cssRules ? Array.from(sheet.cssRules) : [])
+ .filter(rule => rule.type === CSSRule.FONT_FACE_RULE)
+ .map(rule => {
+ const src = parseBackgroundImage(rule.style.getPropertyValue('src'));
+ const formats = [];
+ for (let i = 0; i < src.length; i++) {
+ if (src[i].method === 'url' && src[i + 1] && src[i + 1].method === 'format') {
+ const a = document.createElement('a');
+ a.href = src[i].args[0];
+ if (document.body) {
+ document.body.appendChild(a);
+ }
+
+ const font = {
+ src: a.href,
+ format: src[i + 1].args[0]
+ };
+ formats.push(font);
+ }
+ }
+
+ return {
+ // TODO select correct format for browser),
+
+ formats: formats.filter(font => /^woff/i.test(font.format)),
+ fontFace: rule.style
+ };
+ })
+ .filter(font => font.formats.length);
+};
+
+const createStyleSheetFontsFromText = (text: string, baseHref: string): Array<FontFamily> => {
+ const doc = document.implementation.createHTMLDocument('');
+ const base = document.createElement('base');
+ // $FlowFixMe
+ base.href = baseHref;
+ const style = document.createElement('style');
+
+ style.textContent = text;
+ if (doc.head) {
+ doc.head.appendChild(base);
+ }
+ if (doc.body) {
+ doc.body.appendChild(style);
+ }
+
+ return style.sheet ? getSheetFonts(style.sheet, doc) : [];
+};
+
const restoreOwnerScroll = (ownerDocument: Document, x: number, y: number) => {
if (
ownerDocument.defaultView &&
@@ -415,7 +530,7 @@ export const cloneWindow = (
options: Options,
logger: Logger,
renderer: (element: HTMLElement, options: Options, logger: Logger) => Promise<*>
-): Promise<[HTMLIFrameElement, HTMLElement, ImageLoader<ImageElement>]> => {
+): Promise<[HTMLIFrameElement, HTMLElement, ResourceLoader]> => {
const cloner = new DocumentCloner(referenceElement, options, logger, false, renderer);
const scrollX = ownerDocument.defaultView.pageXOffset;
const scrollY = ownerDocument.defaultView.pageYOffset;
@@ -445,7 +560,7 @@ export const cloneWindow = (
? Promise.resolve([
cloneIframeContainer,
cloner.clonedReferenceElement,
- cloner.imageLoader
+ cloner.resourceLoader
])
: Promise.reject(
__DEV__
View
@@ -146,7 +146,10 @@ const FEATURES = {
// $FlowFixMe - get/set properties not yet supported
get SUPPORT_FOREIGNOBJECT_DRAWING() {
'use strict';
- const value = testForeignObject(document);
+ const value =
+ typeof Array.from === 'function' && typeof window.fetch === 'function'
+ ? testForeignObject(document)
+ : Promise.resolve(false);
Object.defineProperty(FEATURES, 'SUPPORT_FOREIGNOBJECT_DRAWING', {value});
return value;
},
View
@@ -18,7 +18,7 @@ import type {Visibility} from './parsing/visibility';
import type {zIndex} from './parsing/zIndex';
import type {Bounds, BoundCurves} from './Bounds';
-import type ImageLoader, {ImageElement} from './ImageLoader';
+import type ResourceLoader, {ImageElement} from './ResourceLoader';
import type {Path} from './drawing/Path';
import type TextContainer from './TextContainer';
@@ -87,7 +87,7 @@ export default class NodeContainer {
constructor(
node: HTMLElement | SVGSVGElement,
parent: ?NodeContainer,
- imageLoader: ImageLoader<ImageElement>,
+ resourceLoader: ResourceLoader,
index: number
) {
this.parent = parent;
@@ -104,7 +104,7 @@ export default class NodeContainer {
const position = parsePosition(style.position);
this.style = {
- background: IS_INPUT ? INPUT_BACKGROUND : parseBackground(style, imageLoader),
+ background: IS_INPUT ? INPUT_BACKGROUND : parseBackground(style, resourceLoader),
border: IS_INPUT ? INPUT_BORDERS : parseBorder(style),
borderRadius:
(node instanceof defaultView.HTMLInputElement ||
@@ -148,7 +148,7 @@ export default class NodeContainer {
);
});
}
- this.image = getImage(node, imageLoader);
+ this.image = getImage(node, resourceLoader);
this.bounds = IS_INPUT
? reformatInputBounds(parseBounds(node, scrollX, scrollY))
: parseBounds(node, scrollX, scrollY);
@@ -223,26 +223,25 @@ export default class NodeContainer {
}
}
-const getImage = (
- node: HTMLElement | SVGSVGElement,
- imageLoader: ImageLoader<ImageElement>
-): ?string => {
+const getImage = (node: HTMLElement | SVGSVGElement, resourceLoader: ResourceLoader): ?string => {
if (
node instanceof node.ownerDocument.defaultView.SVGSVGElement ||
node instanceof SVGSVGElement
) {
const s = new XMLSerializer();
- return imageLoader.loadImage(
+ return resourceLoader.loadImage(
`data:image/svg+xml,${encodeURIComponent(s.serializeToString(node))}`
);
}
switch (node.tagName) {
case 'IMG':
// $FlowFixMe
- return imageLoader.loadImage(node.currentSrc || node.src);
+ const img: HTMLImageElement = node;
+ return resourceLoader.loadImage(img.currentSrc || img.src);
case 'CANVAS':
// $FlowFixMe
- return imageLoader.loadCanvas(node);
+ const canvas: HTMLCanvasElement = node;
+ return resourceLoader.loadCanvas(canvas);
case 'IFRAME':
const iframeKey = node.getAttribute('data-html2canvas-internal-iframe-key');
if (iframeKey) {
View
@@ -1,6 +1,6 @@
/* @flow */
'use strict';
-import type ImageLoader, {ImageElement} from './ImageLoader';
+import type ResourceLoader, {ImageElement} from './ResourceLoader';
import type Logger from './Logger';
import StackingContext from './StackingContext';
import NodeContainer from './NodeContainer';
@@ -9,7 +9,7 @@ import {inlineInputElement, inlineTextAreaElement, inlineSelectElement} from './
export const NodeParser = (
node: HTMLElement,
- imageLoader: ImageLoader<ImageElement>,
+ resourceLoader: ResourceLoader,
logger: Logger
): StackingContext => {
if (__DEV__) {
@@ -18,10 +18,10 @@ export const NodeParser = (
let index = 0;
- const container = new NodeContainer(node, null, imageLoader, index++);
+ const container = new NodeContainer(node, null, resourceLoader, index++);
const stack = new StackingContext(container, null, true);
- parseNodeTree(node, container, stack, imageLoader, index);
+ parseNodeTree(node, container, stack, resourceLoader, index);
if (__DEV__) {
logger.log(`Finished parsing node tree`);
@@ -36,7 +36,7 @@ const parseNodeTree = (
node: HTMLElement,
parent: NodeContainer,
stack: StackingContext,
- imageLoader: ImageLoader<ImageElement>,
+ resourceLoader: ResourceLoader,
index: number
): void => {
if (__DEV__ && index > 50000) {
@@ -60,7 +60,7 @@ const parseNodeTree = (
(defaultView.parent && childNode instanceof defaultView.parent.HTMLElement)
) {
if (IGNORED_NODE_NAMES.indexOf(childNode.nodeName) === -1) {
- const container = new NodeContainer(childNode, parent, imageLoader, index++);
+ const container = new NodeContainer(childNode, parent, resourceLoader, index++);
if (container.isVisible()) {
if (childNode.tagName === 'INPUT') {
// $FlowFixMe
@@ -92,12 +92,12 @@ const parseNodeTree = (
);
parentStack.contexts.push(childStack);
if (SHOULD_TRAVERSE_CHILDREN) {
- parseNodeTree(childNode, container, childStack, imageLoader, index);
+ parseNodeTree(childNode, container, childStack, resourceLoader, index);
}
} else {
stack.children.push(container);
if (SHOULD_TRAVERSE_CHILDREN) {
- parseNodeTree(childNode, container, stack, imageLoader, index);
+ parseNodeTree(childNode, container, stack, resourceLoader, index);
}
}
}
@@ -107,7 +107,7 @@ const parseNodeTree = (
childNode instanceof SVGSVGElement ||
(defaultView.parent && childNode instanceof defaultView.parent.SVGSVGElement)
) {
- const container = new NodeContainer(childNode, parent, imageLoader, index++);
+ const container = new NodeContainer(childNode, parent, resourceLoader, index++);
const treatAsRealStackingContext = createsRealStackingContext(container, childNode);
if (treatAsRealStackingContext || createsStackingContext(container)) {
// for treatAsRealStackingContext:false, any positioned descendants and descendants
View
@@ -15,7 +15,7 @@ import type {Matrix} from './parsing/transform';
import type {BoundCurves} from './Bounds';
import type {Gradient} from './Gradient';
-import type {ImageStore, ImageElement} from './ImageLoader';
+import type {ResourceStore, ImageElement} from './ResourceLoader';
import type NodeContainer from './NodeContainer';
import type StackingContext from './StackingContext';
import type {TextBounds} from './TextBounds';
@@ -43,7 +43,7 @@ import {BORDER_STYLE} from './parsing/border';
export type RenderOptions = {
scale: number,
backgroundColor: ?Color,
- imageStore: ImageStore<ImageElement>,
+ imageStore: ResourceStore,
fontMetrics: FontMetrics,
logger: Logger,
x: number,
Oops, something went wrong.

0 comments on commit 9445b0b

Please sign in to comment.