print-pdf
Version:
Print DOM element to PDF
354 lines • 15.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const JsPDF = require("jspdf");
function getBinaryPromise(url) {
return new Promise((resolve) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.overrideMimeType('text/plain; charset=x-user-defined');
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(xhr.responseText);
}
else {
resolve('');
}
}
};
xhr.send();
});
}
function base64Encode(str) {
const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const len = str.length;
const out = [];
let i = 0;
let c1;
let c2;
let c3;
while (i < len) {
c1 = str.charCodeAt(i++) & 0xff;
if (i === len) {
out.push(CHARS.charAt(c1 >> 2));
out.push(CHARS.charAt((c1 & 0x3) << 4));
out.push('==');
break;
}
c2 = str.charCodeAt(i++);
if (i === len) {
out.push(CHARS.charAt(c1 >> 2));
out.push(CHARS.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4)));
out.push(CHARS.charAt((c2 & 0xF) << 2));
out.push('=');
break;
}
c3 = str.charCodeAt(i++);
out.push(CHARS.charAt(c1 >> 2));
out.push(CHARS.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4)));
out.push(CHARS.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6)));
out.push(CHARS.charAt(c3 & 0x3F));
}
return out.join('');
}
class PrintPDF {
constructor(element, cfg = {}) {
this.serializer = new XMLSerializer();
this.doc = cfg.document || document;
this.win = cfg.window || window;
this.element = element;
this.uniqueCounter = 0;
this.defaultsMap = new Map();
}
static getMimeType(url) {
const match = /\.([A-z0-9]+?)([?#]+.*)?$/gi.exec(url);
if (!match) {
return 'text/html';
}
return PrintPDF.MIME_TYPES[match[1].toLowerCase()];
}
static zip(...args) {
return args[0].map(({}, i) => args.map((array) => array[i]));
}
static flatMap(elements, callback) {
return elements.reduce((ys, x) => {
return ys.concat(callback.call(this, x));
}, []);
}
getDefaultMap(tagName) {
if (this.defaultsMap.has(tagName)) {
return this.defaultsMap.get(tagName);
}
const el = this.doc.createElement(tagName);
const elMap = new Map();
this.doc.body.appendChild(el);
const computedStyles = this.win.getComputedStyle(el);
Array.from(computedStyles).forEach(prop => {
elMap.set(prop, computedStyles.getPropertyValue(prop));
});
el.remove();
this.defaultsMap.set(tagName, elMap);
return elMap;
}
getUniqueId() {
this.uniqueCounter += 1;
return `print-pdf-${this.uniqueCounter}`;
}
traverseNodes(o, d, applyFn) {
const { filter } = Array.prototype;
applyFn(o, d);
if (o && d) {
const srcNodes = filter.call(o.childNodes, (e) => e instanceof HTMLElement);
const dstNodes = filter.call(d.childNodes, (e) => e instanceof HTMLElement);
if (srcNodes && dstNodes && srcNodes.length === dstNodes.length) {
PrintPDF.zip(srcNodes, dstNodes).forEach((v) => this.traverseNodes(v[0], v[1], applyFn));
}
}
}
copyFontFaces(globalStyle, progressFn) {
return new Promise(resolve => {
const styleSheets = Array.from(this.doc.styleSheets);
const cssRules = PrintPDF.flatMap(styleSheets, (s) => Array.from(s.cssRules));
const ruleType = CSSRule.FONT_FACE_RULE;
const ffRules = cssRules.filter((r) => r.type === ruleType);
const promises = [];
let counter = 0;
ffRules.forEach((r) => {
const styleProperties = Array.from(r.style);
styleProperties.forEach((property) => {
const propValue = r.style.getPropertyValue(property);
const reg = new RegExp(/url\(["']?(.+?)["']?\)/g);
let exec = reg.exec(propValue);
while (exec) {
const match = exec[1];
const baseUrl = r.parentStyleSheet.href || this.element.ownerDocument.location.href;
const url = new URL(match, baseUrl).toString();
const mimeType = PrintPDF.getMimeType(url);
const promChain = getBinaryPromise(url).then(base64Encode).then(enc => {
counter++;
const percent = (counter / promises.length * .20);
const status = `Loading Font Asset ${counter}/${promises.length}`;
progressFn(percent, status);
return { key: url, value: `url("data:${mimeType};base64,${enc}")` };
});
promises.push(promChain);
exec = reg.exec(propValue);
}
});
});
return Promise.all(promises).then(kvList => {
const mapResults = new Map();
kvList.forEach(kv => {
mapResults.set(kv.key, kv.value);
});
return mapResults;
}).then(mapResults => {
ffRules.forEach((r) => {
const styleProperties = Array.from(r.style);
const styleText = styleProperties.map((property) => {
let propValue = r.style.getPropertyValue(property);
if (/url\(["']?[^)]+["']?\)/.test(propValue)) {
const baseUrl = r.parentStyleSheet.href || this.element.ownerDocument.location.href;
propValue = propValue.replace(/url\(["']?(.+?)["']?\)/g, (__, match) => {
const url = new URL(match, baseUrl).toString();
if (mapResults.has(url)) {
return mapResults.get(url);
}
return `url("${match}")`;
});
}
return `\t${property}: ${propValue};`;
}).join('\n');
const textNode = this.doc.createTextNode(`\n@font-face {\n${styleText}\n}\n`);
globalStyle.appendChild(textNode);
resolve();
});
});
});
}
getCSSText(computedStyle, dst, selector = '') {
const { filter } = Array.prototype;
const defaultMap = this.getDefaultMap(dst.tagName);
const textPairs = [];
filter.call(computedStyle, (name) => !PrintPDF.IGNORED_PROPERTIES.has(name))
.forEach((propertyName) => {
const propValue = computedStyle.getPropertyValue(propertyName);
if (selector !== '' || defaultMap.get(propertyName) !== propValue) {
textPairs.push(`\t${propertyName}: ${propValue};`);
}
});
const cssText = textPairs.join('\n');
if (!dst.id) {
dst.id = this.getUniqueId();
dst.className = `print-cn-${dst.tagName}`;
}
return `\n#${dst.id}${selector} {\n${cssText}\n}\n`;
}
toPDF(progressFn = () => { return; }) {
const element = this.element;
let copyElement;
const ceComputedStyle = this.win.getComputedStyle(element);
let width = element.clientWidth;
let height = element.clientHeight;
width += parseInt(ceComputedStyle.getPropertyValue('margin-left'), 10);
width += parseInt(ceComputedStyle.getPropertyValue('margin-right'), 10);
height += parseInt(ceComputedStyle.getPropertyValue('margin-top'), 10);
height += parseInt(ceComputedStyle.getPropertyValue('margin-bottom'), 10);
const globalStyle = this.doc.createElement('style');
globalStyle.type = 'text/css';
const srcDstPairs = [];
const copiedStyles = [];
return new Promise(resolve => {
this.win.setTimeout(() => {
progressFn(0.0, 'Cloning Node');
copyElement = this.doc.importNode(element, true);
copyElement.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml');
resolve();
}, 100);
}).then(() => {
return this.copyFontFaces(globalStyle, progressFn);
}).then(() => {
let counter = 0;
const copySubTaskPercent = .65;
const nodePromises = [];
this.traverseNodes(element, copyElement, (src, dst) => {
srcDstPairs.push({ src, dst });
dst.className = '';
dst.removeAttribute('id');
dst.removeAttribute('style');
counter++;
const itemNumber = counter;
const nodePromise = new Promise(resolve => {
const computedStyle = this.win.getComputedStyle(src);
const text = this.getCSSText(computedStyle, dst);
copiedStyles.push(text);
PrintPDF.PSEUDO_ELEMENTS.forEach((pseudoSelector) => {
const psComputedStyle = this.win.getComputedStyle(src, pseudoSelector);
const content = psComputedStyle.getPropertyValue('content');
if (content !== 'none') {
const psText = this.getCSSText(psComputedStyle, dst, pseudoSelector);
copiedStyles.push(psText);
}
});
resolve(itemNumber);
});
nodePromises.push(nodePromise);
nodePromise.then(c => {
return new Promise(resolve => {
this.win.setTimeout(() => {
const statusMsg = `Reading Computed Style ${c}/${nodePromises.length}`;
const percentComplete = (c / nodePromises.length * copySubTaskPercent) + .2;
progressFn(percentComplete, statusMsg);
resolve();
}, 1);
});
});
});
return Promise.all(nodePromises);
}).then(() => {
this.defaultsMap.forEach((innerMap, tagName) => {
const textPairs = [];
innerMap.forEach((propValue, propertyName) => {
textPairs.push(`\t${propertyName}: ${propValue};`);
});
const styleText = `\n.print-cn-${tagName} {\n${textPairs.join('\n')}\n}\n`;
copiedStyles.push(styleText);
});
const stylesText = copiedStyles.join('');
const textNode = this.doc.createTextNode(stylesText);
globalStyle.appendChild(textNode);
const promises = srcDstPairs.map((pair) => {
const { src, dst } = pair;
return new Promise((resolve) => {
['scrollLeft', 'scrollTop', 'value'].forEach((prop) => {
dst[prop] = src[prop];
});
resolve();
});
});
return Promise.all(promises);
}).then(() => {
progressFn(.85, 'Appending Master StyleSheet');
copyElement.appendChild(globalStyle);
return copyElement;
}).then(copy => {
progressFn(.87, 'Serializing');
return this.serializer.serializeToString(copy);
}).then(serialized => {
progressFn(.88, 'Encoding to Data URI');
const encoded = encodeURIComponent(serialized);
const foreignObject = `<foreignObject width='100%' height='100%'>${encoded}</foreignObject>`;
const namespace = 'http://www.w3.org/2000/svg';
const svgMarkup = `<svg xmlns='${namespace}' width='${width}' height='${height}'>${foreignObject}</svg>`;
return `data:image/svg+xml,${svgMarkup}`;
}).then(dataUri => {
return new Promise(resolve => {
this.win.setTimeout(() => {
progressFn(.90, 'Creating Canvas');
const tmpCanvas = this.doc.createElement('canvas');
tmpCanvas.width = width;
tmpCanvas.height = height;
const tmpCtx = tmpCanvas.getContext('2d', { alpha: false });
tmpCtx.fillStyle = '#ffffff';
tmpCtx.fillRect(0, 0, width, height);
const img = new Image();
img.onload = () => {
progressFn(.95, 'Drawing');
tmpCtx.drawImage(img, 0, 0, width, height);
const dataURL = tmpCanvas.toDataURL('image/jpeg', 1.0);
resolve(dataURL);
};
img.src = dataUri;
}, 10);
});
}).then(dataURL => {
progressFn(.98, 'Creating PDF');
const orientation = (width > height) ? 'l' : 'p';
const pdf = new JsPDF(orientation, 'pt', [width, height]);
const w = Math.floor(pdf.internal.pageSize.getWidth());
const h = Math.floor(pdf.internal.pageSize.getHeight());
pdf.addImage(dataURL, 'PNG', 0, 0, w, h);
progressFn(1.0, 'Done');
return pdf;
});
}
printPDF(progressFn) {
return this.toPDF(progressFn).then((pdf) => {
const fileURL = URL.createObjectURL(pdf.output('blob'));
const win = window.open(fileURL, '_blank');
win.focus();
return pdf;
});
}
}
PrintPDF.PSEUDO_ELEMENTS = [
'::after',
'::before'
];
PrintPDF.IGNORED_PROPERTIES = new Set([
'animation',
'animation-delay',
'animation-direction',
'animation-duration',
'animation-fill-mode',
'animation-iteration',
'animation-name',
'animation-play-state: ',
'animation-timing-function',
'animation-iteration-count',
'animation-play-state',
'transition-delay',
'transition-duration',
'transition-property',
'transition-timing-function',
'cursor'
]);
PrintPDF.MIME_TYPES = {
woff: 'application/font-woff',
woff2: 'application/font-woff',
eot: 'application/vnd.ms-fontobject',
ttf: 'application/font-sfnt',
svg: 'image/svg+xml'
};
exports.PrintPDF = PrintPDF;
//# sourceMappingURL=PrintPDF.js.map