UNPKG

highcharts

Version:
398 lines (397 loc) 15.8 kB
/* * * * Client side exporting module * * (c) 2015 Torstein Honsi / Oystein Moseng * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ 'use strict'; import AST from '../../Core/Renderer/HTML/AST.js'; import Chart from '../../Core/Chart/Chart.js'; import D from '../../Core/Defaults.js'; const { getOptions, setOptions } = D; import DownloadURL from '../DownloadURL.js'; const { downloadURL, getScript } = DownloadURL; import G from '../../Core/Globals.js'; const { composed, doc, win } = G; import OfflineExportingDefaults from './OfflineExportingDefaults.js'; import U from '../../Core/Utilities.js'; const { addEvent, extend, pushUnique } = U; /* * * * Composition * * */ var OfflineExporting; (function (OfflineExporting) { /* * * * Functions * * */ /** * Composition function. * * @private * @function compose * * @param {ExportingClass} ExportingClass * Exporting class. * * @requires modules/exporting * @requires modules/offline-exporting */ function compose(ExportingClass) { // Add the downloadSVG event to the Exporting class for local PDF export addEvent(ExportingClass, 'downloadSVG', async function (e) { const { svg, exportingOptions, exporting, preventDefault } = e; // Check if PDF export is requested if (exportingOptions?.type === 'application/pdf') { // Prevent the default export behavior preventDefault?.(); // Run the PDF local export try { // Get the final image options const { type, filename, scale, libURL } = G.Exporting.prepareImageOptions(exportingOptions); // Local PDF download if (type === 'application/pdf') { // Must load pdf libraries first if not found. Don't // destroy the object URL yet since we are doing // things asynchronously if (!win.jspdf?.jsPDF) { // Get jspdf await getScript(`${libURL}jspdf.js`); // Get svg2pdf await getScript(`${libURL}svg2pdf.js`); } // Call the PDF download if SVG element found await downloadPDF(svg, scale, filename, exportingOptions?.pdfFont); } } catch (error) { // Try to fallback to the server await exporting?.fallbackToServer(exportingOptions, error); } } }); // Check the composition registry for the OfflineExporting if (!pushUnique(composed, 'OfflineExporting')) { return; } // Adding wrappers for the deprecated functions extend(Chart.prototype, { exportChartLocal: async function (exportingOptions, chartOptions) { await this.exporting?.exportChart(exportingOptions, chartOptions); return; } }); // Update with defaults of the offline exporting module setOptions(OfflineExportingDefaults); // Additionaly, extend the menuItems with the offline exporting variants const menuItems = getOptions().exporting?.buttons?.contextButton?.menuItems; menuItems && menuItems.push('downloadPDF'); } OfflineExporting.compose = compose; /** * Get data URL to an image of an SVG and call download on it options * object: * - **filename:** Name of resulting downloaded file without extension. * Default is `chart`. * * - **type:** File type of resulting download. Default is `image/png`. * * - **scale:** Scaling factor of downloaded image compared to source. * Default is `1`. * - **libURL:** URL pointing to location of dependency scripts to download * on demand. Default is the exporting.libURL option of the global * Highcharts options pointing to our server. * * @function Highcharts.downloadSVGLocal * @deprecated * * @param {string} svg * The generated SVG * * @param {Highcharts.ExportingOptions} options * The exporting options * */ async function downloadSVGLocal(svg, options) { await G.Exporting.prototype.downloadSVG.call(void 0, svg, options); } OfflineExporting.downloadSVGLocal = downloadSVGLocal; /** * Converts an SVG string into a PDF file and triggers its download. This * function processes the SVG, applies necessary font adjustments, converts * it to a PDF, and initiates the file download. * * @private * @async * @function downloadPDF * * @param {string} svg * A string representation of the SVG markup to be converted into a PDF. * @param {number} scale * The scaling factor for the PDF output. * @param {string} filename * The name of the downloaded PDF file. * @param {Highcharts.PdfFontOptions} [pdfFont] * An optional object specifying URLs for different font variants (normal, * bold, italic, bolditalic). * * @return {Promise<void>} * A promise that resolves when the PDF has been successfully generated and * downloaded. * * @requires modules/exporting * @requires modules/offline-exporting */ async function downloadPDF(svg, scale, filename, pdfFont) { const svgNode = preparePDF(svg, pdfFont); if (svgNode) { // Loads all required fonts await loadPdfFonts(svgNode, pdfFont); // Transform SVG to PDF const pdfData = await svgToPdf(svgNode, 0, scale); // Download the PDF downloadURL(pdfData, filename); } } /** * Loads and registers custom fonts for PDF export if non-ASCII characters * are detected in the given SVG element. This function ensures that text * content with special characters is properly rendered in the exported PDF. * * It fetches font files (if provided in `pdfFont`), converts them to * base64, and registers them with jsPDF. * * @private * @function loadPdfFonts * * @param {SVGElement} svgElement * The generated SVG element containing the text content to be exported. * @param {Highcharts.PdfFontOptions} [pdfFont] * An optional object specifying URLs for different font variants (normal, * bold, italic, bolditalic). If non-ASCII characters are not detected, * fonts are not loaded. * * @requires modules/exporting * @requires modules/offline-exporting */ async function loadPdfFonts(svgElement, pdfFont) { const hasNonASCII = (s) => ( // eslint-disable-next-line no-control-regex /[^\u0000-\u007F\u200B]+/.test(s)); // Register an event in order to add the font once jsPDF is initialized const addFont = (variant, base64) => { win.jspdf.jsPDF.API.events.push([ 'initialized', function () { this.addFileToVFS(variant, base64); this.addFont(variant, 'HighchartsFont', variant); if (!this.getFontList()?.HighchartsFont) { this.setFont('HighchartsFont'); } } ]); }; // If there are no non-ASCII characters in the SVG, do not use bother // downloading the font files if (pdfFont && !hasNonASCII(svgElement.textContent || '')) { pdfFont = void 0; } // Add new font if the URL is declared, #6417 const variants = ['normal', 'italic', 'bold', 'bolditalic']; // Shift the first element off the variants and add as a font. // Then asynchronously trigger the next variant until variants are empty let normalBase64; for (const variant of variants) { const url = pdfFont?.[variant]; if (url) { try { const response = await win.fetch(url); if (!response.ok) { throw new Error(`Failed to fetch font: ${url}`); } const blob = await response.blob(), reader = new FileReader(); const base64 = await new Promise((resolve, reject) => { reader.onloadend = () => { if (typeof reader.result === 'string') { resolve(reader.result.split(',')[1]); } else { reject(new Error('Failed to read font as base64')); } }; reader.onerror = reject; reader.readAsDataURL(blob); }); addFont(variant, base64); if (variant === 'normal') { normalBase64 = base64; } } catch (e) { // If fetch or reading fails, fallback to next variant } } else { // For other variants, fall back to normal text weight/style if (normalBase64) { addFont(variant, normalBase64); } } } } /** * Prepares an SVG for PDF export by ensuring proper text styling and * removing unnecessary elements. This function extracts an SVG element from * a given SVG string, applies font styles inherited from parent elements, * and removes text outlines and title elements to improve PDF rendering. * * @private * @function preparePDF * * @param {string} svg * A string representation of the SVG markup. * @param {Highcharts.PdfFontOptions} [pdfFont] * An optional object specifying URLs for different font variants (normal, * bold, italic, bolditalic). If provided, the text elements are assigned a * custom PDF font. * * @return {SVGSVGElement | null} * Returns the parsed SVG element from the container or `null` if the SVG is * not found. * * @requires modules/exporting * @requires modules/offline-exporting */ function preparePDF(svg, pdfFont) { const dummySVGContainer = doc.createElement('div'); AST.setElementHTML(dummySVGContainer, svg); const textElements = dummySVGContainer.getElementsByTagName('text'), // Copy style property to element from parents if it's not there. // Searches up hierarchy until it finds prop, or hits the chart // container setStylePropertyFromParents = function (el, propName) { let curParent = el; while (curParent && curParent !== dummySVGContainer) { if (curParent.style[propName]) { let value = curParent.style[propName]; if (propName === 'fontSize' && /em$/.test(value)) { value = Math.round(parseFloat(value) * 16) + 'px'; } el.style[propName] = value; break; } curParent = curParent.parentNode; } }; let titleElements, outlineElements; // Workaround for the text styling. Making sure it does pick up // settings for parent elements. [].forEach.call(textElements, function (el) { // Workaround for the text styling. making sure it does pick up // the root element ['fontFamily', 'fontSize'] .forEach((property) => { setStylePropertyFromParents(el, property); }); el.style.fontFamily = pdfFont?.normal ? // Custom PDF font 'HighchartsFont' : // Generic font (serif, sans-serif etc) String(el.style.fontFamily && el.style.fontFamily.split(' ').splice(-1)); // Workaround for plotband with width, removing title from text // nodes titleElements = el.getElementsByTagName('title'); [].forEach.call(titleElements, function (titleElement) { el.removeChild(titleElement); }); // Remove all .highcharts-text-outline elements, #17170 outlineElements = el.getElementsByClassName('highcharts-text-outline'); while (outlineElements.length > 0) { const outline = outlineElements[0]; if (outline.parentNode) { outline.parentNode.removeChild(outline); } } }); return dummySVGContainer.querySelector('svg'); } /** * Transform from PDF to SVG. * * @async * @private * @function svgToPdf * * @param {Highcharts.SVGElement} svgElement * The SVG element to convert. * @param {number} margin * The margin to apply. * @param {number} scale * The scale of the SVG. * * @requires modules/exporting * @requires modules/offline-exporting */ async function svgToPdf(svgElement, margin, scale) { const width = (Number(svgElement.getAttribute('width')) + 2 * margin) * scale, height = (Number(svgElement.getAttribute('height')) + 2 * margin) * scale, pdfDoc = new win.jspdf.jsPDF(// eslint-disable-line new-cap // Setting orientation to portrait if height exceeds width height > width ? 'p' : 'l', 'pt', [width, height]); // Workaround for #7090, hidden elements were drawn anyway. It comes // down to https://github.com/yWorks/svg2pdf.js/issues/28. Check this // later. [].forEach.call(svgElement.querySelectorAll('*[visibility="hidden"]'), function (node) { node.parentNode.removeChild(node); }); // Workaround for #13948, multiple stops in linear gradient set to 0 // causing error in Acrobat const gradients = svgElement.querySelectorAll('linearGradient'); for (let index = 0; index < gradients.length; index++) { const gradient = gradients[index]; const stops = gradient.querySelectorAll('stop'); let i = 0; while (i < stops.length && stops[i].getAttribute('offset') === '0' && stops[i + 1].getAttribute('offset') === '0') { stops[i].remove(); i++; } } // Workaround for #15135, zero width spaces, which Highcharts uses // to break lines, are not correctly rendered in PDF. Replace it // with a regular space and offset by some pixels to compensate. [].forEach.call(svgElement.querySelectorAll('tspan'), (tspan) => { if (tspan.textContent === '\u200B') { tspan.textContent = ' '; tspan.setAttribute('dx', -5); } }); // Transform from PDF to SVG await pdfDoc.svg(svgElement, { x: 0, y: 0, width, height, removeInvalid: true }); // Return the output return pdfDoc.output('datauristring'); } })(OfflineExporting || (OfflineExporting = {})); /* * * * Default Export * * */ export default OfflineExporting;