UNPKG

print-pdf

Version:
354 lines 15.1 kB
"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