JSReports = function(input){ // Require html2canvas.min.js, purify.min.js & jspdf.umd.min.js for PDF files. const self = this, default_settings = { nulls : false, default_value : null, timeout : 2000, cache_box : "body", preload_show_exception : true, preload_timeout : 2000, frames_per_second : 24, default_cache_box : "body", width : 17, margin_top : 20, margin_left : 20, margin_right : 20, margin_bottom : 20, // margin : [50, 50, 50, 40], // [top, right, bottom, left] proxy : null, autostart : true, hash_alphabet : "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz", hash_length : 13, default_list_mode : "ul", dpi : 96, page_format : "a4", margin_footer : 5, margin_header : 5, header_height : 30, footer_height : 10, date_format : "{dd}/{mm}/{yyyy} {hh}:{ii}:{ss}", months : [ "January", "Febrary", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ], week_days : [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ], // pdf.addFont("/data/OpenSans-Regular.ttf", "Open Sans", "normal"); default_fonts : [ ["FA5FB", "https://cdn.k3y.pw/fonts/FontAwesome/5.15/fa-brands-400.ttf", "normal"], ["FA5FR", "https://cdn.k3y.pw/fonts/FontAwesome/5.15/fa-regular-400.ttf", "normal"], ["FA5FS", "https://cdn.k3y.pw/fonts/FontAwesome/5.15/fa-solid-900.ttf", "normal"], // ["Open Sans Condensed", "https://cdn.k3y.pw/fonts/Open_Sans/static/OpenSans_Condensed/OpenSans_Condensed-Bold.ttf", "bold"], // ["Open Sans Condensed", "https://cdn.k3y.pw/fonts/Open_Sans/static/OpenSans_Condensed/OpenSans_Condensed-Medium.ttf", "normal"], // ["Open Sans Condensed", "https://cdn.k3y.pw/fonts/Open_Sans/static/OpenSans_Condensed/OpenSans_Condensed-Italic.ttf", "italic"], // ["Open Sans", "https://cdn.k3y.pw/fonts/Open_Sans/static/OpenSans/OpenSans-Bold.ttf", "bold"], // ["Open Sans", "https://cdn.k3y.pw/fonts/Open_Sans/static/OpenSans/OpenSans-Medium.ttf", "normal"], // ["Open Sans", "https://cdn.k3y.pw/fonts/Open_Sans/static/OpenSans/OpenSans-Italic.ttf", "italic"] ["Open Sans", "https://cdn.k3y.pw/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf", "normal"], ["Oxygen", "https://cdn.k3y.pw/fonts/Oxygen/Oxygen-Regular.ttf", "normal"], ["Roboto", "https://cdn.k3y.pw/fonts/Roboto/Roboto-Medium.ttf", "normal"], ["Roboto Mono", "https://cdn.k3y.pw/fonts/Roboto_Mono/RobotoMono-VariableFont_wght.ttf", "normal"], ["Source Code Pro", "https://cdn.k3y.pw/fonts/Source_Code_Pro/SourceCodePro-VariableFont_wght.ttf", "normal"], ["Ubuntu", "https://cdn.k3y.pw/fonts/Ubuntu/Ubuntu-Medium.ttf", "normal"], ["Ubuntu Mono", "https://cdn.k3y.pw/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf", "normal"] ] }, events = {}, hashes = [], fonts = {}; let started = false, cache_box = document, proxy = null, thread = null, is_ready = false, months = [], week_days = []; const event_execute = this.event_execute = key => events[key] && events[key].forEach(event => event && event()); const event_add = this.event_add = (key, method) => { if(!key || typeof method != "function") return null; let i = 0; const l = (events[key] || (events[key] = [])).length; for(; i < l; i ++) if(!events[key][i]) break; events[key][i] = method; return i; }; const event_remove = this.event_remove = (key, i) => key && !isNaN(i) && events[key] && events[key][i] && (events[key][i] = null); const is_html_item = this.is_html_item = item => item && (item.tagName || item.nodeName); const default_value = this.default_value = (_default, nulls) => _default !== undefined && (_default !== null || (typeof nulls == "boolean" ? nulls : settings("nulls", null, false, false))) ? _default : settings(["default_value", "default"], null, null, true); const settings = this.settings = (names, inputs, _default, nulls) => { if(!names) return default_value(_default, nulls); const l = (names instanceof Array ? names : names = [names]).length, m = (inputs = (typeof inputs == "object" ? inputs instanceof Array ? inputs : [inputs] : []).concat([input, default_settings])).length; typeof nulls != "boolean" && (nulls = settings("nulls", null, false, false)); for(let j = 0; j < m; j ++) if(inputs[j] && typeof inputs[j] == "object") for(let i = 0; i < l; i ++) if(names[i] && inputs[j][names[i]] !== undefined && (nulls || inputs[j][names[i]] !== null)) return inputs[j][names[i]]; return default_value(_default, nulls); }; const threads_method = () => event_execute("threads"); const threads_start = this.threads_start = frames_per_second => thread === null && (thread = setInterval(threads_method, 1000 / (isNaN(frames_per_second) ? settings(["frames_per_second", "fps"]) : frames_per_second))); this.threads_stop = () => { if(thread === null) return; clearInterval(thread); thread = null; }; const threads_add = this.threads_add = method => event_add("threads", method); const threads_remove = this.threads_remove = i => event_remove("threads", i); const preload = this.preload = (selector, callback) => { if(typeof callback != "function") return; if(!selector){ callback(null, false, "NO_CALLBACK"); return; }; if(is_html_item(selector)){ callback(selector, false, "OK"); return; }; if(typeof selector != "string"){ callback(null, false, "NOT_SELECTOR"); return; }; let item; try{ if(item = document.querySelector(selector)){ callback(item, false, "OK"); return; }; }catch(exception){ settings(["preload_show_exception", "show_exception"]) && console.error(excetpion); callback(null, false, "BAD_SELECTOR"); return; }; const date = Date.now(), timeout = settings(["preload_timeout", "timeout"]), thread = threads_add(() => { if(item = document.querySelector(selector)){ callback(item, true, "OK"); threads_remove(thread); }else if(Date.now() - date > timeout){ callback(null, true, "TIMEOUT"); threads_remove(thread); }; }); }; const ready = position => { is_ready = true; cache_box = position; event_execute("on_ready"); }; this.start = () => { if(started) return; started = true; threads_start(); proxy = settings("proxy"); preload(settings("cache_box"), position => position ? ready(position) : preload(settings("default_cache_box"), ready)); }; this.on_ready = method => is_ready ? method() : event_add("on_ready", method); const string_variables = this.string_variables = (string, variables, _default) => { const l = (variables = variables ? variables instanceof Array ? variables : typeof variables == "object" ? [variables] : [] : []).length; return string.replace(/\{([^\{\}]+)\}/g, (...arguments) => { for(let i = 0; i < l; i ++) if(variables[i][arguments[1]] !== undefined) return variables[i][arguments[1]]; return _default !== undefined ? _default : arguments[0]; }); }; const base64_to_blob = this.base64_to_blob = (uri, type) => { const array = atob(uri.split(",")[1]), l = array.length, buffer = new ArrayBuffer(l), integers = new Uint8Array(buffer); for(let i = 0; i < l; i ++) buffer[i] = array.charCodeAt(i); return new Blob([buffer], {type : type}); }; const hash = this.hash = () => { const alphabet = settings(["hash_alphabet", "alphabet"]), l = alphabet.length, length = settings(["hash_length", "length"]); let hash; do{ hash = ""; while((hash += alphabet[Math.random() * l >> 0]).length < length); }while( hashes.includes(hash) || /^[0-9]/.test(hash) || document.querySelector("." + hash + ",#" + hash + ",[name=" + hash + "]") ); hashes.push(hash); return hash; }; const grid_to_html = this.grid_to_html = (grid, name) => { let html = (` `); grid.header.forEach((header, j) => html += ``); html += (` `); grid.body.forEach((row, i) => { html += ``; row.forEach((value, j) => html += ``); html += ``; }); html += (`
` + header + `
` + value + `
`); return html; }; const list_to_html = this.list_to_html = (list, name) => { const mode = { ordered : "ol", unordered : "ul" }[list.mode] || list.mode || settings(["default_list_mode", "list_mode"]); let html = `<` + mode + ` class="list` + (name ? " " + name : "") + `"` + (isNaN(list.start) ? `` : ` start="` + list.start + `"`) + `>`; list.items.forEach((item, i) => html += `
  • ` + (typeof item == "object" ? item[0] + list_to_html(item[1]) : item) + `
  • `); html += ``; return html; }; const css_to_html = this.css_to_html = (html, css, type, body_size, dpi, callback) => { const html_item = cache_box.appendChild(document.createElement("div")), cache = cache_box.appendChild(document.createElement("div")), images_items = {}, images_data = {}, preload_hash = hash(), is_doc = type == "doc"; preload("[data-preload=" + preload_hash + "]", preloader => { preloader.remove(); let loaded = 0; const images = html_item.querySelectorAll("img"), l = images.length, end = (url, image) => { if(image){ const canvas = cache.appendChild(document.createElement("canvas")), context = canvas.getContext("2d"); let unit; canvas.setAttribute("width", image.width); canvas.setAttribute("height", image.height); context.drawImage(image, 0, 0, image.width, image.height); images_data[url] = canvas.toDataURL("image/png", 0.9); images_items[url].forEach(item => { item.setAttribute("src", images_data[url]); if(!is_doc) return; if(item.style.width == "auto" && item.style.height && item.style.height != "auto"){ unit = item.style.height.match(/[^0-9\.]+$/)[0]; item.style.width = unit == "%" ? (body_size.height * parseInt(item.style.height) / 1000) + "cm" : (parseInt(item.style.height) * image.width / image.height) + unit; }else if(item.style.height == "auto" && item.style.width && item.style.width != "auto"){ unit = item.style.width.match(/[^0-9\.]+$/)[0]; item.style.height = unit == "%" ? (body_size.width * parseInt(item.style.width) / 1000) + "cm" : (parseInt(item.style.width) * image.height / image.width) + unit; }; }); canvas.remove(); }; if(++ loaded < l) return; cache.remove(); html = html_item.innerHTML; html_item.remove(); callback(html); }; (css ? css instanceof Array ? css : [css] : []).forEach(sheet => sheet && sheet.replace(/\/\*(([^\*]+|[\r\n]+|\*[^\/])*)(\*\/)?|(([^\{]+|[\r\n]+)*)\{(([^\}]+|[\r\n]+)*)\}/g, (...arguments) => { if(arguments[1]) return; html_item.querySelectorAll(arguments[5].trim()).forEach(item => { arguments[6].replace(/([^\s\:]+)\s*\:([^;\}]+)/g, (...subarguments) => item.style[subarguments[1].trim().replace(/\-(.)/g, (all, capital) => capital.toUpperCase())] = subarguments[2]); item.tagName && item.tagName.toLowerCase() == "table" && item.style.width && !item.hasAttribute("width") && (item.setAttribute("width", item.style.width)); }); })); is_doc && html_item.querySelectorAll("table").forEach(table => { if(table.querySelector("colgroup")) return; const group = table.insertBefore(document.createElement("colgroup"), table.querySelector("tbody,thead,tfoot")), sizes = []; let total = 0, unsized = 0, medium = 0; table.querySelector("tr").childNodes.forEach(column => { if(!column || column.substr || !column.tagName || !["th", "td"].includes(column.tagName.toLowerCase())) return; const matches = column.style.width ? column.style.width.match(/^([0-9\.]+)([^0-9\.]+)$/) : null, l = column.hasAttribute("colspan") ? Number(column.getAttribute("colspan")) || 1 : 1; if(!matches){ unsized += l; sizes.push(null); return; }; let size = parseInt(matches[1]); switch(matches[2].toLowerCase()){ case "px": size *= dpi / 25.4; break; case "mm": size *= 100 / body_size.width; break; case "cm": size *= 10 / body_size.width; break; case "em": case "ex": case "in": size *= 2540 / body_size.width; break; case "%": break; }; total += size; size /= l; for(let i = 0; i < l; i ++) sizes[sizes.length] = size; }); medium = total < 100 ? (100 - total) / unsized : 0; total < 100 && (total = 100); sizes.forEach(size => group.appendChild(document.createElement("col")).setAttribute("width", 500 * (size === null ? medium : size) / total)); }); if(!images.length){ end(); return; }; images.forEach(item => { const url = string_variables(proxy || "{url}", {url : item.getAttribute("src")}); if(images_data[url]){ ++ loaded; item.setAttribute("src", images_data[url]); return; }; if(images_items[url]){ ++ loaded; images_items[url].push(item); return; }; let image = new Image(); images_items[url] = [item]; image.src = url; image.crossOrigin = "anonymous"; image.onload = () => end(url, image); image.onerror = () => end(url, null); }); }); cache.style.position = html_item.style.position = "absolute"; cache.style.left = html_item.style.left = "100%"; html_item.innerHTML = html + `
    `; }; const get_page_size = this.get_page_size = format => { switch(format = format.toLowerCase()){ case "dl": return [99, 210]; case "letter": return [216, 279]; case "legal": return [216, 356]; }; const matches = format.match(/^([abc])(10|[0-9])$/); if(!matches) return null; const [_, type, size] = matches, results = { a : [26.3, 37.16], b : [31.25, 44.2], c : [28.66, 40.535] }[type]; for(let i = 9, temporary; i >= size; i --){ temporary = results[0]; results[0] = results[1]; results[1] = 2 * temporary; }; return results.map(x => x >> 0); }; this.to_pixels = (size, dpi) => size * (dpi || settings("dpi")) / 25.1; const create_canvas = this.create_canas = (html, width, height, callback) => { const iframe = cache_box.appendChild(document.createElement("iframe")), on_load_callback = event => { const body = (iframe.contentDocument || iframe.contentWindow.document).body; body.innerHTML = (`
    ` + html + `
    `); new html2canvas(body, { scale : 2, backgroundColor : null, width : width, height : height }).then(canvas => { iframe.remove(); const uri = canvas.toDataURL("image/png", .9), context = canvas.getContext("2d"); callback(canvas.toDataURL("image/png", .9)); }); }; iframe.setAttribute("style", "position:absolute;left:100%;top:100%;opacity:0;"); // iframe.addEventListener("load", on_load_callback); setTimeout(on_load_callback, 50); }; const date_format = this.date_format = this.datetime_format = this.time_format = (date, format) => { const year = (date || (date = new Date())).getFullYear(), month = date.getMonth() + 1, day = date.getDate(), hour = date.getHours(), minute = date.getMinutes(), second = date.getSeconds(), timestamp = date.getTime(), milliseconds = timestamp % 1000, week_day = date.getDay(); return string_variables(format || (format = settings("date_format")), { yyyy : ("0000" + year).slice(-4), yy : ("00" + (year % 100)).slice(-2), y : year, year : year, mmmm : months[month], mmm : months[month].substr(0, 3), mm : ("00" + month).slice(-2), m : month, month : month, dd : ("00" + day).slice(-2), d : day, day : day, hh : ("00" + hour).slice(-2), h : hour, hour : hour, ii : ("00" + minute).slice(-2), i : minute, minute : minute, ss : ("00" + second).slice(-2), s : second, second : second, nnn : ("000" + milliseconds).slice(-3), n : milliseconds, milliseconds : milliseconds, www : week_days[week_day], ww : week_days[week_day].substr(0, 3), w : week_day, week_day : week_day, weekday : week_day }); }; const page_variables = (html, variables) => html.replace(/\{([^\{\}]+)\}/g, (...arguments) => { if(variables[arguments[1]]) return variables[arguments[1]]; const matches = arguments[1].match(/^([^\:]+)(\:(.+)?)?$/); if(matches) switch(matches[1]){ case "date": case "datetime": case "time": return date_format(new Date(), matches[3] ? matches[3].replace(/[\[\]]/g, (...arguments) => arguments[0] == "]" ? "]" : "[") : settings([matches[1] + "_format", "date_format"])) }; return arguments[0]; }); const pdf_header_footer_create = (pdf, header, footer, margin, pages, callback, i) => { const has_header = header, has_footer = footer, m = (has_header ? 1 : 0) + (has_footer ? 1 : 0); if(!m || i > pages){ typeof callback == "function" && callback(pdf); return; }; let j = 0; const end = () => { if(++ j < m) return; pdf_header_footer_create(pdf, header, footer, margin, pages, callback, i + 1); }, width = margin.page_width - margin.left - margin.right, variables = { page : i, pages : pages }; pdf.setPage(i); has_header && create_canvas(page_variables(header[i % header.length], variables), width, margin.header_height, uri => { pdf.addImage(uri, "PNG", margin.left, margin.header_top, width, margin.header_height); end(); }); has_footer && create_canvas(page_variables(footer[i % footer.length], variables), width, margin.footer_height, uri => { pdf.addImage(uri, "PNG", margin.left, margin.footer_top, width, margin.footer_height); end(); }); }; this.create = (input, callback) => { if(!input) input = {}; else if(typeof input == "string") input = {body : input}; else if(typeof input != "object") input = {}; let body = `
    ` + settings(["body", "html"], input, ``) + `
    `, header = settings(["header", "head"], input), footer = settings(["footer", "foot"], input), ended = false, i = 0, css = settings(["css", "styles", "style"], input), margin_processed; const variables = [{}, input.variables || {}, input], type = settings("type", input), margin = { top : settings(["margin_top", "margin"], input), left : settings(["margin_left", "margin"], input), right : settings(["margin_right", "margin"], input), bottom : settings(["margin_bottom", "margin"], input) }, page_format = settings("page_format", input), page_size = get_page_size(page_format), body_size = { width : page_size[0] - margin.left - margin.right, height : page_size[1] - margin.top - margin.bottom }, dpi = settings(["dpi", "ppp", "dpp"], input), l = ( (header ? (header.push ? header : header = [header]).length : 0) + (footer ? (footer.push ? footer : footer = [footer]).length : 0) + 1 ), end = () => { if(ended || ++ i < l) return; ended = true; body = string_variables(body, variables); header && header.map(html => string_variables(html, variables)); footer && footer.map(html => string_variables(html, variables)); switch(type){ case "pdf": const pdf = new window.jspdf.jsPDF({ unit : "px", format : page_format, hotfixes : ["px_scaling"] }), width = settings(["width", "width_" + page_format], input), fonts_added = []; let key; css && css.forEach(string => string.replace(/font-family\s*\:\s*([^;]+)/g, (...arguments) => { arguments[1].replace(/"([^"]+)"|'([^']+)'|([^\s]+)/g, (...subarguments) => { fonts[key = subarguments[1] || subarguments[2] || subarguments[3]] && fonts[key].forEach(font => pdf.addFont(font[0], key, font[1])); }); })); pdf.html(body, { callback : pdf => pdf_header_footer_create(pdf, header, footer, margin, pdf.internal.getNumberOfPages(), callback, 1), // width : width, margin : margin_processed, autoPaging : "text", html2canvas : { letterRendering : 1, allowTaint : true, useCORS : true, logging : true } }); break; case "doc": /* xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:w='urn:schemas-microsoft-com:office:word' xmlns:m="http://schemas.microsoft.com/office/2004/12/omml" xmlns='http://www.w3.org/TR/REC-html40' xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:w='urn:schemas-microsoft-com:office:word' xmlns='http://www.w3.org/TR/REC-html40' */ callback(page_variables(string_variables((` ` + (settings("title", input) || "") + `
    {header} {footer} {body}
    `), [{ header : header && header.length ? `
    ` + header[0] + `
    ` : ``, footer : footer && footer.length ? `
    ` + footer[0] + `
    ` : ``, body : body }].concat(variables)), { page : `1`, pages : `1` })); break; }; }; header && header.map(html => `
    ` + html + `
    `); footer && footer.map(html => `
    ` + html + `
    `); for(const key in variables[1]) if(variables[1][key] && typeof variables[1][key] == "object" && variables[1][key].type) switch(variables[1][key].type){ case "grid": case "table": variables[0][key] = grid_to_html(variables[1][key], key); break; case "list": variables[0][key] = list_to_html(variables[1][key], key); break; }; if(css) !(css instanceof Array) && (css = [css]); else css = []; switch(type){ case "pdf": const header_height = header ? settings("header_height", input) : 0, footer_height = footer ? settings("footer_height", input) : 0; margin.header_top = margin.top + 0; margin.header_height = header_height; margin.header_bottom = page_size[1] - margin.top - header_height; margin.footer_top = page_size[1] - margin.bottom - footer_height; margin.footer_height = footer_height; margin.footer_bottom = margin.bottom + 0; margin.page_height = page_size[1]; margin.page_width = page_size[0]; header && (margin.top += header_height + settings("margin_header", input)); footer && (margin.bottom += footer_height + settings("margin_footer", input)); for(const key in margin) margin[key] = dpi * margin[key] / 25.4; margin_processed = [ margin.top, margin.right, margin.bottom, margin.left ]; css.push(` .header,.body,.footer{width : ` + ((dpi * page_size[0] / 25.4) - margin.left - margin.right) + `px;} .header{ position : absolute; top : 0px; left : 0px; } `); break; case "doc": break; }; css_to_html(string_variables(body, variables), css, type, body_size, dpi, html => {body = html;end();}); header && header.forEach((html, i) => css_to_html(string_variables(html, variables), css, type, body_size, dpi, html => {header[i] = html;end();})); footer && footer.forEach((html, i) => css_to_html(string_variables(html, variables), css, type, body_size, dpi, html => {footer[i] = html;end();})); }; const get_uri_data = this.get_uri_data = (data, type) => { if(typeof data == "string" && data.match(/^data\:[^,]+,[^,]+$/)) return data; if(data instanceof Blob) return URL.createObjectURL(data); switch(type){ case "pdf": return data.output("datauristring"); case "doc": return "data:application/vnd.ms-word;charset=utf-8," + encodeURIComponent(data); default: return "data:application/octetstream;base64," + encodeURIComponent(btoa(typeof data == "object" ? JSON.stringify(data) : "" + data)) }; return null; }; this.download = (data, file_name) => { const anchor = cache_box.appendChild(document.createElement("a")); anchor.setAttribute("download", file_name); anchor.setAttribute("target", "_blank"); anchor.setAttribute("href", get_uri_data(data, file_name.match(/\.([^\.]+)$/)[1].toLowerCase())); anchor.click(); anchor.remove(); // window.open(get_uri_data(data, file_name.match(/\.([^\.]+)$/)[1].toLowerCase()), "_blank"); }; const add_fonts = this.add_fonts = array => array && array.push && array.forEach(font => { if(!fonts[font[0]]) fonts[font[0]] = [[font[1], font[2]]]; else{ let i; const exists = fonts[font[0]].some((source, j) => { if(source[1] == font[2]){ i = j; return true; }; }); if(exists) fonts[font[0]][i][0] = font[1]; else fonts[font[0]].push([font[1], font[2]]); }; }); const construct = () => { week_days = settings("week_days"); months = settings("months"); ["default_fonts", "fonts"].forEach(key => add_fonts(settings(key))); settings("autostart") && self.start(); }; construct(); };