// ==UserScript== // @name Wenku Doc Downloader // @namespace http://tampermonkey.net/ // @version 1.8.5 // @description 对文档截图,合并为纯图片PDF。有限地支持(1)豆丁网 (2)道客巴巴 (3)360个人图书馆(4)得力文库 (5)MBA智库(6)爱问文库(7)原创力文档(8)读根网(9)国标网(10)安全文库网(11)人人文库(12)云展网(13)360文库(14)技工教育网(15)文库吧(16)中国社会科学文库。在网页左侧中间有按钮区和小猴子图标,说明脚本生效了。预览多少页,导出多少页。额外支持(1)食典通(2)JJG 计量技术规范,详见下方说明。 // @author 2690874578@qq.com // @match *://*.docin.com/p-* // @match *://docimg1.docin.com/?wk=true // @match *://ishare.iask.sina.com.cn/f/* // @match *://ishare.iask.com/f/* // @match *://swf.ishare.down.sina.com.cn/?path=* // @match *://swf.ishare.down.sina.com.cn/?wk=true // @match *://www.deliwenku.com/p-* // @match *://file.deliwenku.com/?num=* // @match *://file3.deliwenku.com/?num=* // @match *://www.doc88.com/p-* // @match *://www.360doc.com/content/* // @match *://doc.mbalib.com/view/* // @match *://www.dugen.com/p-* // @match *://max.book118.com/html/* // @match *://openapi.book118.com/?* // @match *://view-cache.book118.com/pptView.html?* // @match *://*.book118.com/?readpage=* // @match *://c.gb688.cn/bzgk/gb/showGb?* // @match *://www.safewk.com/p-* // @match *://www.renrendoc.com/paper/* // @match *://www.renrendoc.com/p-* // @match *://www.yunzhan365.com/basic/* // @match *://book.yunzhan365.com/*index.html* // @match *://www.bing.com/search?q=Bing+AI&showconv=1* // @match *://wenku.so.com/d/* // @match *://jg.class.com.cn/cms/resourcedetail.htm?contentUid=* // @match *://preview.imm.aliyuncs.com/index.html?url=*/jgjyw/* // @match *://www.wenkub.com/p-*.html* // @match *://www.sklib.cn/manuscripts/?* // @match *://www.jinchutou.com/shtml/view-* // @match *://www.jinchutou.com/p-* // @require https://cdn.staticfile.org/jspdf/2.5.1/jspdf.umd.min.js // @require https://cdn.staticfile.org/html2canvas/1.4.1/html2canvas.min.js // @icon https://s2.loli.net/2022/01/12/wc9je8RX7HELbYQ.png // @icon64 https://s2.loli.net/2022/01/12/tmFeSKDf8UkNMjC.png // @grant none // @run-at document-idle // @license GPL-3.0-only // @create 2021-11-22 // @note 1. 重写豆丁网导出PDF功能 // @downloadURL none // ==/UserScript== (function () { 'use strict'; const ping = Symbol("ping"), pong = Symbol("pong"), onPing = Symbol("onPing"), onPong = Symbol("onPong"); /** * 基于 window.postMessage 通信的套接字对象 */ class Socket { /** * 创建套接字对象 * @param {Window} target 目标窗口 */ constructor(target) { if (!(target.window && (target === target.window))) { console.log(target); throw new Error(`target is not a [Window Object]`); } this.target = target; this.connected = false; this.listeners = new Set(); } get [Symbol.toStringTag]() { return "Socket"; } /** * 向目标窗口发消息 * @param {*} message */ talk(message) { if (!this.target) { throw new TypeError( `socket.target is not a window: ${this.target}` ); } this.target.postMessage(message, "*"); } /** * 添加捕获型监听器,返回实际添加的监听器 * @param {Function} listener (e: MessageEvent) => {...} * @param {boolean} once 是否在执行后自动销毁,默认 false;如为 true 则使用自动包装过的监听器 * @returns {Function} listener */ listen(listener, once=false) { if (this.listeners.has(listener)) { return; } let real_listener = listener; // 包装监听器 if (once) { const self = this; function wrapped(e) { listener(e); self.notListen(wrapped); } real_listener = wrapped; } // 添加监听器 this.listeners.add(real_listener); window.addEventListener( "message", real_listener, true ); return real_listener; } /** * 移除socket上的捕获型监听器 * @param {Function} listener (e: MessageEvent) => {...} */ notListen(listener) { console.log(listener); console.log( "listener delete operation:", this.listeners.delete(listener) ); window.removeEventListener("message", listener, true); } /** * 检查对方来信是否为pong消息 * @param {MessageEvent} e * @param {Function} resolve */ [onPong](e, resolve) { // 收到pong消息 if (e.data.pong) { this.connected = true; this.listeners.forEach( listener => listener.ping ? this.notListen(listener) : 0 ); console.log("Client: Connected!\n" + new Date()); resolve(this); } } /** * 向对方发送ping消息 * @returns {Promise} */ [ping]() { return new Promise((resolve, reject) => { // 绑定pong检查监听器 const listener = this.listen( e => this[onPong](e, resolve) ); listener.ping = true; // 5分钟后超时 setTimeout( () => reject(new Error(`Timeout Error during receiving pong (>5min)`)), 5 * 60 * 1000 ); // 发送ping消息 this.talk({ ping: true }); }); } /** * 检查对方来信是否为ping消息 * @param {MessageEvent} e * @param {Function} resolve */ [onPing](e, resolve) { // 收到ping消息 if (e.data.ping) { this.target = e.source; this.connected = true; this.listeners.forEach( listener => listener.pong ? this.notListen(listener) : 0 ); console.log("Server: Connected!\n" + new Date()); // resolve 后期约状态无法回退 // 但后续代码仍可执行 resolve(this); // 回应pong消息 this.talk({ pong: true }); } } /** * 当对方来信是为ping消息时回应pong消息 * @returns {Promise} */ [pong]() { return new Promise(resolve => { // 绑定ping检查监听器 const listener = this.listen( e => this[onPing](e, resolve) ); listener.pong = true; }); } /** * 连接至目标窗口 * @param {boolean} talk_first 是否先发送ping消息 * @param {Window} target 目标窗口 * @returns {Promise} */ connect(talk_first) { // 先发起握手 if (talk_first) { return this[ping](); } // 后发起握手 return this[pong](); } } const base = { Socket, // 用于取得一次列表中所有迭代器的值 getAllValus: function(iterators) { if (iterators.length === 0) { return [true, []]; } let values = []; for (let iterator of iterators) { let {value, done} = iterator.next(); if (done) { return [true, []]; } values.push(value); } return [false, values]; }, /** * 使用过时的execCommand复制文字 * @param {string} text */ oldCopy: function(text) { const input = document.createElement("input"); input.value = text; document.body.appendChild(input); input.select(); document.execCommand("copy"); input.remove(); }, b64ToUint6: function(nChr) { return nChr > 64 && nChr < 91 ? nChr - 65 : nChr > 96 && nChr < 123 ? nChr - 71 : nChr > 47 && nChr < 58 ? nChr + 4 : nChr === 43 ? 62 : nChr === 47 ? 63 : 0; }, /** * 元素选择器 * @param {string} selector 选择器 * @returns {Array} 元素列表 */ $: function(selector) { const self = this?.querySelectorAll ? this : document; return [...self.querySelectorAll(selector)]; }, /** * 安全元素选择器,直到元素存在时才返回元素列表,最多等待5秒 * @param {string} selector 选择器 * @returns {Promise>} 元素列表 */ $$: async function(selector) { const self = this?.querySelectorAll ? this : document; for (let i = 0; i < 10; i++) { let elems = [...self.querySelectorAll(selector)]; if (elems.length > 0) { return elems; } await new Promise(r => setTimeout(r, 500)); } throw Error(`"${selector}" not found in 5s`); }, /** * 将2个及以上的空白字符(除了换行符)替换成一个空格 * @param {string} text * @returns {string} */ stripBlanks: function(text) { return text .replace(/([^\r\n])(\s{2,})(?=[^\r\n])/g, "$1 ") .replace(/\n{2,}/, "\n"); }, /** * 复制属性(含访问器)到 target * @param {Object} target * @param {...Object} sources * @returns */ superAssign: function(target, ...sources) { sources.forEach(source => Object.defineProperties( target, Object.getOwnPropertyDescriptors(source) ) ); return target; }, makeCRC32: function() { function makeCRCTable() { let c; let crcTable = []; for(var n =0; n < 256; n++){ c = n; for(var k =0; k < 8; k++){ c = ((c&1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1)); } crcTable[n] = c; } return crcTable; } const crcTable = makeCRCTable(); /** * @param {string} str * @returns {number} */ return function(str) { let crc = 0 ^ (-1); for (var i = 0; i < str.length; i++ ) { crc = (crc >>> 8) ^ crcTable[(crc ^ str.charCodeAt(i)) & 0xFF]; } return (crc ^ (-1)) >>> 0; }; } }; globalThis.wk$ = base.$; globalThis.wk$$ = base.$$; const utils = { Socket: base.Socket, /** * raise an error for status which is not in [200, 299] * @param {Response} response */ raise_for_status(response) { if (!response.ok) { throw new Error( `Fetch Error with status code: ${response.status}` ); } }, /** * 计算 str 的 CRC32 摘要(number) * @param {string} str * @returns {number} */ crc32: base.makeCRC32(), /** * 返回函数参数定义 * @param {Function} fn * @param {boolean} print 是否打印到控制台,默认 true * @returns {string | undefined} */ help: function(fn, print=true) { if (!(fn instanceof Function)) throw new Error(`fn must be a function`); const ARROW_ARG = /^([^(]+?)=>/, FN_ARGS = /^[^(]*\(\s*([^)]*)\)/m, STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg, fn_text = Function.prototype.toString.call(fn).replace(STRIP_COMMENTS, ''), args = fn_text.match(ARROW_ARG) || fn_text.match(FN_ARGS), // 如果自带 doc,优先使用,否则使用源码 doc = fn.__doc__ ? fn.__doc__ : args[0]; if (!print) return base.stripBlanks(doc); const color = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ) ; console.log("%c" + doc, `color: ${color}; font: small italic`); }, /** * 字节数组转十六进制字符串 * @param {Uint8Array} arr * @returns {string} */ uint8ArrToHexStr: function(arr) { return Array.from(arr) .map(byte => byte.toString(16).padStart(2, "0")) .join(""); }, /** * 取得对象类型 * @param {*} obj * @returns {string} class */ classof: function(obj) { return Object .prototype .toString .call(obj) .slice(8, -1); }, /** * 随机改变字体颜色、大小、粗细 * @param {HTMLElement} elem */ emphasizeText: function(elem) { const rand = Math.random; elem.style.cssText = ` font-weight: ${200 + parseInt(700 * rand())}; font-size: ${(1 + rand()).toFixed(1)}em; color: hsl(${parseInt(360 * rand())}, ${parseInt(40 + 60 * rand())}%, ${parseInt(60 * rand())}%); background-color: yellow;`; }, /** * 等待直到 DOM 节点停止变化 * @param {HTMLElement} elem 监听节点 * @param {number} timeout 超时毫秒数 * @returns {Promise} observer */ untilDOMStill: async function(elem, timeout=2000) { // 创建用于共享的监听器 let observer; // 创建超时 Promise const timeout_promise = new Promise((_, reject) => { setTimeout(() => { // 停止监听、释放资源 observer.disconnect(); const error = new Error( `Timeout Error occured on listening DOM mutation (max ${timeout}ms)`, { cause: elem } ); reject(error); }, timeout); }); // 开始元素节点变动监听 return Promise.race([ new Promise(resolve => { // 创建监听器 observer = new MutationObserver( (_, observer) => { // DOM 变动结束后终止监听、释放资源 observer.disconnect(); // 返回监听器 resolve(observer); } ); // 开始监听目标节点 observer.observe(elem, { subtree: true, childList: true, attributes: true }); }), timeout_promise, ]) .catch(error => { if (`${error}`.includes("Timeout Error")) { return observer; } console.error(error); throw error; }); }, /** * 返回子数组位置,找不到返回-1 * @param {Array} arr 父数组 * @param {Array} sub_arr 子数组 * @param {number} from 开始位置 * @returns {number} index */ indexOfSubArray: function(arr, sub_arr, from) { // 如果子数组为空,则返回-1 if (sub_arr.length === 0) return -1; // 初始化当前位置为from let position = from; // 算出最大循环次数 const length = arr.length - sub_arr.length + 1; // 循环查找子数组直到没有更多 while (position < length) { // 如果当前位置的元素与子数组的第一个元素相等,则开始比较后续元素 if (arr[position] === sub_arr[0]) { // 初始化匹配标志为真 let match = true; // 循环比较后续元素,如果有不相等的,则将匹配标志设为假,并跳出循环 for (let i = 1; i < sub_arr.length; i++) { if (arr[position + i] !== sub_arr[i]) { match = false; break; } } // 如果匹配标志为真,则说明找到了子数组,返回当前位置 if (match) return position; } // 更新当前位置为下一个位置 position++; } // 如果循环结束还没有找到子数组,则返回-1 return -1; }, /** * 用文件头切断文件集合体 * @param {Uint8Array} data * @param {Uint8Array} head 默认 null,即使用 data 前 8 字节 * @returns {Array} files */ splitFiles: function(data, head=null) { // 创建一个空数组用于存放结果 const files = []; // 初始化当前位置为0 let pos = 0; let next_pos = 0; // 确定 head head = head ? head : data.slice(0, 8); // 循环查找PNG标识直到没有更多 while (next_pos < data.length) { // 查找下一个PNG标识的位置 next_pos = this.indexOfSubArray( data, head, pos + 1 ); let image; if (next_pos === -1) { // 如果没有找到,则说明已经到达最后一张图片,将其复制并添加到结果中 image = data.slice(pos); files.push(image); break; } image = data.slice(pos, next_pos); files.push(image); // 更新当前位置为下一个PNG标识的位置 pos = next_pos; } // 返回结果数组 return files; }, /** * 函数装饰器:仅执行一次 func * @param {Function} func 无参无返回值函数 * @returns {Function} */ once: function(func) { return async function() { let used = false; if (!used) { used = true; return func(); } } }, /** * 将类似于dict的简单object转为get请求的queryString * @param {Object} dict * @returns {string} */ dictToQueryStr: function(dict) { const params = []; for (let prop in dict) { params.push(`${prop}=${dict[prop]}`); } return params.join("&"); }, /** * 返回一个包含计数器的迭代器, 其每次迭代值为 [index, value] * @param {Iterable} iterable * @returns */ enumerate: function* (iterable) { let i = 0; for (let value of iterable) { yield [i, value]; i++; } }, /** * 同步的迭代若干可迭代对象 * @param {...Iterable} iterables * @returns */ zip: function* (...iterables) { // 强制转为迭代器 let iterators = iterables.map( iterable => iterable[Symbol.iterator]() ); // 逐次迭代 while (true) { let [done, values] = base.getAllValus(iterators); if (done) { return; } if (values.length === 1) { yield values[0]; } else { yield values; } } }, /** * 返回指定范围整数生成器 * @param {number} end 如果只提供 end, 则返回 [0, end) * @param {number} end2 如果同时提供 end2, 则返回 [end, end2) * @param {number} step 步长, 可以为负数,不能为 0 * @returns */ range: function*(end, end2=null, step=1) { // 参数合法性校验 if (step === 0) { throw new RangeError("step can't be zero"); } const len = end2 - end; if (end2 && len && step && (len * step < 0)) { throw new RangeError(`[${end}, ${end2}) with step ${step} is invalid`); } // 生成范围 end2 = end2 === null ? 0 : end2; let [small, big] = [end, end2].sort((a, b) => a - b); // 开始迭代 if (step > 0) { for (let i = small; i < big; i += step) { yield i; } } else { for (let i = big; i > small; i += step) { yield i; } } }, /** * 获取整个文档的全部css样式 * @returns {string} css text */ getAllStyles: function() { let styles = []; for (let sheet of document.styleSheets) { let rules; try { rules = sheet.cssRules; } catch(e) { if (!(e instanceof DOMException)) { console.error(e); } continue; } for (let rule of rules) { styles.push(rule.cssText); } } return styles.join("\n\n"); }, /** * 复制text到剪贴板 * @param {string} text * @returns */ copyText: function(text) { // 输出到控制台和剪贴板 console.log(text); if (!navigator.clipboard) { base.oldCopy(text); return; } navigator.clipboard.writeText(text) .catch(_ => base.oldCopy(text)); }, /** * 复制媒体到剪贴板 * @param {Blob} blob */ copyData: async function(blob) { const data = [new ClipboardItem({ [blob.type]: blob })]; try { await navigator.clipboard.write(data); console.log(`${blob.type} 成功复制到剪贴板`); } catch (err) { console.error(err.name, err.message); } }, /** * 创建并下载文件 * @param {string} file_name 文件名 * @param {ArrayBuffer | ArrayBufferView | Blob | string} content 内容 * @param {string} type 媒体类型,需要符合 MIME 标准 */ saveAs: function(file_name, content, type="") { const blob = new Blob( [content], { type } ); const size = (blob.size / 1024).toFixed(1); console.log(`blob saved, size: ${size} kb, type: ${blob.type}`); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.download = file_name; a.href = url; a.click(); URL.revokeObjectURL(url); }, /** * 显示/隐藏按钮区 */ toggleBtnsBox: function() { let sec = wk$(".wk-box")[0]; if (sec.style.display === "none") { sec.style.display = "block"; return; } sec.style.display = "none"; }, /** * 异步地睡眠 delay 毫秒, 可选 max_delay 控制波动范围 * @param {number} delay 等待毫秒 * @param {number} max_delay 最大等待毫秒, 默认为null * @returns */ sleep: async function(delay, max_delay=null) { max_delay = max_delay === null ? delay : max_delay; delay = delay + (max_delay - delay) * Math.random(); return new Promise(resolve => setTimeout(resolve, delay)); }, /** * 允许打印页面 */ allowPrint: function() { const style = document.createElement("style"); style.innerHTML = ` @media print { body { display: block; } }`; document.head.append(style); }, /** * 取得get参数key对应的value * @param {string} key * @returns {string} value */ getParam: function(key) { return new URL(location.href).searchParams.get(key); }, /** * 求main_set去除cut_set后的set * @param {Iterable} main_set * @param {Iterable} cut_set * @returns 差集 */ diff: function(main_set, cut_set) { const _diff = new Set(main_set); for (let elem of cut_set) { _diff.delete(elem); } return _diff; }, /** * 增强按钮(默认为蓝色按钮:展开文档)的点击效果 * @param {string} i 按钮序号 */ enhanceBtnClick: async function(i) { let btn = this.btn(i); const style = btn.getAttribute("style") || ""; // 变黑缩小 btn.setAttribute( "style", style + "color: black; font-weight: normal;" ); await utils.sleep(500); btn = this.btn(i); // 复原加粗 btn.setAttribute("style", style); }, /** * 绑定事件处理函数到指定按钮,返回实际添加的事件处理函数 * @param {(btn: HTMLButtonElement) => Promise} listener click监听器,第一个参数是按钮的引用 * @param {number} i 按钮序号 * @param {string} new_text 按钮的新文本,为null则不替换 * @returns {Function} 事件处理函数 */ setBtnListener: function(listener, i, new_text=null) { const btn = this.btn(i); // 如果需要,替换按钮内文本 if (new_text) { btn.textContent = new_text; } const _listener = async () => { const btn = this.btn(i); await listener(btn); await this.enhanceBtnClick(i); btn.textContent = new_text; }; // 绑定事件,添加到页面上 btn.addEventListener("click", _listener, true); return _listener; }, /** * 返回第 index 个按钮引用 * @param {number} i * @returns {HTMLButtonElement} */ btn: function(i) { return wk$(`.wk-box [class="btn-${i}"]`)[0]; }, /** * 强制隐藏元素 * @param {string} selector */ forceHide: function(selector) { const cls = "force-hide"; wk$(selector).forEach(elem => { elem.classList.add(cls); }); // 判断css样式是否已经存在 let style = wk$(`style.${cls}`)[0]; // 如果已经存在,则无须重复创建 if (style) { return; } // 否则创建 style = document.createElement("style"); style.innerHTML = `style.${cls} { visibility: hidden !important; display: none !important; }`; document.head.append(style); }, /** * 等待直到元素可见。最多等待5秒。 * @param {HTMLElement} elem 一个元素 * @returns {Promise} elem */ untilVisible: async function(elem) { let [max, i] = [25, 0]; let style = getComputedStyle(elem); // 如果不可见就等待0.2秒/轮 while (i <= max && (style.display === "none" || style.visibility !== "hidden") ) { i++; style = getComputedStyle(elem); await this.sleep(200); } return elem; }, /** * 等待直到函数返回true * @param {Function | Promise} isReady 判断条件达成与否的函数 * @param {number} timeout 最大等待秒数, 默认5000毫秒 */ waitUntil: async function(isReady, timeout=5000) { const gap = 200; let chances = parseInt(timeout / gap); chances = chances < 1 ? 1 : chances; while (! await isReady()) { await this.sleep(200); chances -= 1; if (!chances) { break; } } }, /** * 隐藏按钮,打印页面,显示按钮 */ hideBtnThenPrint: async function() { // 隐藏按钮,然后打印页面 this.toggleBtnsBox(); await this.sleep(1000); print(); this.toggleBtnsBox(); }, /** * 切换按钮显示/隐藏状态 * @param {number} i 按钮序号 * @returns 按钮元素的引用 */ toggleBtn: function(i) { const btn = this.btn(i); const display = getComputedStyle(btn).display; if (display === "none") { btn.style.display = "block"; } else { btn.style.display = "none"; } return btn; }, /** * 用input框跳转到对应页码 * @param {Element} cur_page 当前页码 * @param {string | Number} aim_page 目标页码 * @param {string} event_type 键盘事件类型:"keyup" | "keypress" | "keydown" */ toPageNo: function(cur_page, aim_page, event_type) { // 设置跳转页码为目标页码 cur_page.value = `${aim_page}`; // 模拟回车事件来跳转 let keyboard_event_enter = new KeyboardEvent(event_type, { bubbles: true, cancelable: true, keyCode: 13 }); cur_page.dispatchEvent(keyboard_event_enter); }, /** * 判断给定的url是否与当前页面同源 * @param {string} url * @returns {boolean} */ isSameOrigin: function(url) { url = new URL(url); if (url.protocol === "data:") { return true; } if (location.protocol === url.protocol && location.host === url.host && location.port === url.port ) { return true; } return false; }, /** * 在新标签页打开链接,如果提供文件名则下载 * @param {string} url * @param {string} fname 下载文件的名称,默认为空,代表不下载 */ openURL: function(url, fname="") { const a = document.createElement("a"); a.href = url; a.target = "_blank"; if (fname && this.isSameOrigin(url)) { a.download = fname; } a.click(); }, /** * 用try移除元素 * @param {Element} element 要移除的元素 */ remove: function(element) { try { element.remove(); } catch (e) {} }, /** * 用try移除若干元素 * @param {Iterable} elements 要移除的元素列表 */ removeMulti: function(elements) { Array.from(elements).forEach(elem => this.remove(elem) ); }, /** * 使文档在页面上居中 * @param {string} selector 文档容器的css选择器 * @param {string} default_offset 文档部分向右偏移的百分比(0-59) * @returns 偏移值是否合法 */ centerDoc: function(selector, default_offset) { const doc_main = wk$(selector)[0]; const offset = prompt("请输入偏移百分位:", default_offset); // 如果输入的数字不在 0-59 内,提醒用户重新设置 if (offset.length === 1 && offset.search(/[0-9]/) !== -1) { doc_main.style.marginLeft = offset + "%"; return true; } else if (offset.length === 2 && offset.search(/[1-5][0-9]/) !== -1) { doc_main.style.marginLeft = offset + "%"; return true } else { alert("请输入一个正整数,范围在0至59之间,用来使文档居中\n(不同文档偏移量不同,所以需要手动调整)"); return false; } }, /** * 等待全部任务落定后返回值的列表 * @param {Iterable} tasks * @returns {Promise} values */ gather: async function(tasks) { const results = await Promise.allSettled(tasks); return results .filter(result => result.value) .map(result => result.value); }, /** * html元素列表转为canvas列表 * @param {ArrayLike} elements * @returns {Promise>} */ elementsToCanvases: async function(elements) { if (!globalThis.html2canvas) { await this.loadWebScript( "https://cdn.staticfile.org/html2canvas/1.4.1/html2canvas.min.js" ); } // 如果是空列表, 则抛出异常 if (elements.length === 0) { throw new Error("htmlToCanvases 未得到任何html元素"); } return this.gather( Array.from(elements).map(html2canvas) ); }, /** * 将html元素转为canvas再合并到pdf中,最后下载pdf * @param {ArrayLike} elements html元素列表 * @param {string} title 文档标题 */ elementsToPDF: async function(elements, title = "文档") { // 如果是空元素列表,终止函数 const canvases = await this.elementsToCanvases(elements); // 控制台检查结果 console.log("生成的canvas元素如下:"); console.log(canvases); // 合并为PDF this.imgsToPDF(canvases, title); }, /** * 使用xhr异步GET请求目标url,返回响应体blob * @param {string} url * @returns {Promise} blob */ xhrGetBlob: async function(url) { const xhr = new XMLHttpRequest(); xhr.open("GET", url); xhr.responseType = "blob"; return new Promise((resolve, reject) => { xhr.onload = () => { const code = xhr.status; if (code >= 200 && code <= 299) { resolve(xhr.response); } else { reject(new Error(`Network Error: ${code}`)); } }; xhr.send(); }); }, /** * 加载CDN脚本 * @param {string} url */ loadWebScript: async function(url) { try { // xhr+eval方式 Function( await (await this.xhrGetBlob(url)).text() )(); } catch(e) { console.error(e); // 嵌入