可知网导出页面到PDF,仅对PDF预览有效
// ==UserScript== // @name keledge-helper // @namespace http://tampermonkey.net/ // @version 0.2 // @description 可知网导出页面到PDF,仅对PDF预览有效 // @author [email protected] // @match https://www.keledge.com/pdfReader?* // @require https://cdn.staticfile.net/pdf-lib/1.17.1/pdf-lib.min.js // @require https://cdn.staticfile.net/sweetalert2/11.10.3/sweetalert2.all.min.js // @icon https://www.google.com/s2/favicons?sz=64&domain=keledge.com // @grant none // @run-at document-start // @license GPL-3.0-only // ==/UserScript== (function () { "use strict"; // 全局常量 const GUI = `<div><style class="keledge-style">.keledge-fold-btn{position:fixed;left:151px;top:36%;user-select:none;font-size:large;z-index:1001}.keledge-fold-btn::after{content:"🐵"}.keledge-fold-btn.folded{left:20px}.keledge-fold-btn.folded::after{content:"🙈"}.keledge-box{position:fixed;width:154px;left:10px;top:32%;z-index:1000}.btns-sec{background:#e7f1ff;border:2px solid #1676ff;padding:0 0 10px 0;font-weight:600;border-radius:2px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Hiragino Sans GB','Microsoft YaHei','Helvetica Neue',Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol'}.btns-sec.folded{display:none}.logo-title{width:100%;background:#1676ff;text-align:center;font-size:large;color:#e7f1ff;line-height:40px;height:40px;margin:0 0 16px 0}.keledge-box button{display:block;width:128px;height:28px;border-radius:4px;color:#fff;font-size:12px;border:none;outline:0;margin:8px auto;font-weight:700;cursor:pointer;opacity:.9}.keledge-box button.folded{display:none}.keledge-box .btn-1{background:linear-gradient(180deg,#00e7f7 0,#feb800 .01%,#ff8700 100%)}.keledge-box .btn-1:hover,.keledge-box .btn-2:hover{opacity:.8}.keledge-box .btn-1:active,.keledge-box .btn-2:active{opacity:1}</style><div class="keledge-box"><section class="btns-sec"><p class="logo-title">keledge-helper</p><button class="btn-1" onclick="btn1_fn(this)">{{btn1_desc}}</button></section><p class="keledge-fold-btn" onclick="[this, this.parentElement.querySelector('.btns-sec')].forEach(elem => elem.classList.toggle('folded'))"></p></div></div>`; const pdf_data_map = new Map(); const println = console.log.bind(console); const logs = []; // 全局变量 let page_index = -1; // 全局属性 Object.assign(window, { println, pdf_data_map }); function log(...args) { const time = new Date().toTimeString().split(" ")[0]; const record = `[${time}]\t${args}`; logs.push(record); println(...args); } function clear_pdf_data() { const size = pdf_data_map.size; pdf_data_map.clear(); log(`PDF缓存已清空,共清理 ${size} 页`); } /** * @param {number} delay */ function sleep(delay) { return new Promise((resolve) => setTimeout(resolve, delay)); } /** * @param {string[]} libs */ async function wait_for_libs(libs) { let not_ready = true; while (not_ready) { for (const lib of libs) { if (!window[lib]) { not_ready = true; break; } else { not_ready = false; } } await sleep(200); } } /** * 替换 window.glob_obj_name.method 为 new_method * @param {string} glob_obj_name * @param {string} method * @param {Function} new_method */ function hook_method(glob_obj_name, method, new_method) { const obj = window[glob_obj_name]; window[method] = obj[method].bind(obj); window["_" + glob_obj_name] = obj; window[glob_obj_name] = new Proxy(obj, { get(target, property, _) { if (property === method) { println( `代理并替换了 ${glob_obj_name}.${property} 属性(方法)访问` ); return new_method; } return target[property]; }, }); } function hooked_get_doc(pdf_data) { // debugger; if (!pdf_data_map.has(page_index)) { pdf_data_map.set(page_index, pdf_data.data); log(`已经捕获数量:${pdf_data_map.size}`); } return window["getDocument"](pdf_data); } function hook_pdfjs() { hook_method("pdfjsLib", "getDocument", hooked_get_doc); } /** * @param {{ id: string, container: HTMLDivElement, eventBus: any, "110n": any, linkService: any, textLayerMode: number }} config */ function hooked_viewer(config) { // id: "pdf-page-0" page_index = parseInt(config.id.split("-").at(-1)); log(`正在加载页面:${page_index + 1}`); return new window["PDFViewer"](config); } function hook_viewer() { hook_method("pdfjsViewer", "PDFViewer", hooked_viewer); } /** * 返回一个包含计数器的迭代器, 其每次迭代值为 [index, value] * @param {Iterable} iterable * @returns */ function* enumerate(iterable) { let i = 0; for (let value of iterable) { yield [i, value]; i++; } } async function myalert(text) { return Sweetalert2.fire({ text, icon: "error", allowOutsideClick: false, }); } /** * 合并多个PDF * @param {Array<ArrayBuffer | Uint8Array>} pdfs * @returns {Promise<Uint8Array>} */ async function join_pdfs(pdfs) { if (!window.PDFLib) { const url = "https://cdn.staticfile.org/pdf-lib/1.17.1/pdf-lib.min.js"; const code = await fetch(url).then((resp) => resp.text()); eval(code); } if (!window.PDFLib) { const msg = "缺少 PDFLib 无法导出 PDF!"; myalert(msg); throw new Error(msg); } const combined = await PDFLib.PDFDocument.create(); for (const [i, buffer] of enumerate(pdfs)) { const pdf = await PDFLib.PDFDocument.load(buffer); const pages = await combined.copyPages(pdf, pdf.getPageIndices()); for (const page of pages) { combined.addPage(page); } log(`已经合并 ${i + 1} 组`); } return combined.save(); } /** * 创建并下载文件 * @param {string} file_name 文件名 * @param {ArrayBuffer | ArrayBufferView | Blob | string} content 内容 * @param {string} type 媒体类型,需要符合 MIME 标准 */ function save(file_name, content, type = "") { const blob = new Blob([content], { type }); const size = (blob.size / ####).toFixed(1); 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); } /** * @param {string} text * @returns {Promise<boolean>} */ async function myconfirm(text) { const r###lt = await Sweetalert2.fire({ text, icon: "warning", showCancelButton: true, confirmButtonColor: "#3085d6", cancelButtonColor: "#d33", allowOutsideClick: false, }); return r###lt.isConfirmed; } async function export_pdf() { const yes = await myconfirm("是否导出已经捕获的页面?导出后会清空缓存"); if (!yes) { return; } // 每个 Item 是 [页码, 数据] const pdfs = Array.from(pdf_data_map) .sort((a, b) => a[0] - b[0]) .map((item) => item[1]); const combined = await join_pdfs(pdfs); save(document.title + ".pdf", combined, "application/pdf"); clear_pdf_data(); } function show_tips() { Sweetalert2.fire({ title: "可知助手小提示", html: "<p>以下快捷键可用: </p><p>显示帮助: ALT + H</p><p>导出文档: ALT + S</p><p>显示日志: ALT + L</p><p>进度明细: ALT + P</p><p>清空缓存: ALT + C</p>", timer: 10000, timerProgressBar: true, allowOutsideClick: true, }); } /** * 按下 alt + h 弹出帮助文档 * @param {KeyboardEvent} event */ function shortcut_alt_h(event) { if (!(event.altKey && event.code === "KeyH")) { return; } show_tips(); } /** * 按下 alt + s 以导出PDF * @param {KeyboardEvent} event */ function shortcut_alt_s(event) { if (!(event.altKey && event.code === "KeyS")) { return; } export_pdf(); } /** * 按下 alt + l 以显示日志 * @param {KeyboardEvent} event */ function shortcut_alt_l(event) { if (!(event.altKey && event.code === "KeyL")) { return; } const text = logs.join("\n"); Sweetalert2.fire({ title: "可知助手日志", html: `<textarea readonly rows="10" cols="50" style="resize: none;">${text}</textarea>`, showConfirmButton: false, }); } /** * 描述整数数组 * @param {number[]} nums * @returns {string} */ function desc_num_arr(nums) { const r###lt = []; let start = null; let end = null; for (let i = 0; i < nums.length; i++) { if (start === null) { start = nums[i]; end = nums[i]; } else if (nums[i] === end + 1) { end = nums[i]; } else { if (start === end) { r###lt.push(`${start}`); } else { r###lt.push(`${start}-${end}`); } start = nums[i]; end = nums[i]; } } if (start !== null) { if (start === end) { r###lt.push(start.toString()); } else { r###lt.push(`${start}-${end}`); } } return r###lt.join(", "); } /** * 按下 alt + p 以显示进度详情 * @param {KeyboardEvent} event */ function shortcut_alt_p(event) { if (!(event.altKey && event.code === "KeyP")) { return; } const captured = Array .from(pdf_data_map.keys()) .sort((a, b) => a - b) .map(pn => pn + 1); const progress = desc_num_arr(captured); Sweetalert2.fire({ title: "页面捕获进度", text: captured.length ? `已经捕获的页码:${progress}` : `尚未捕获任何页面`, }); } /** * 按下 alt + c 以显示进度详情 * @param {KeyboardEvent} event */ async function shortcut_alt_c(event) { if (!(event.altKey && event.code === "KeyC")) { return; } const hint = `是否清空所有已经捕获的页面(共 ${pdf_data_map.size} 页)?`; const yes = await myconfirm(hint); if (!yes) { return; } clear_pdf_data(); Sweetalert2.fire({ icon: "info", text: "缓存已清空", }); } async function early_main() { log("进入 keledge-helper 脚本"); await wait_for_libs(["pdfjsLib", "pdfjsViewer"]); hook_viewer(); hook_pdfjs(); window.btn1_fn = export_pdf; const gui = GUI.replace("{{btn1_desc}}", "导出PDF"); document.body.insertAdjacentHTML("beforeend", gui); } function set_shortcuts() { const shortcuts = [ shortcut_alt_h, // 显示帮助 shortcut_alt_s, // 导出pdf shortcut_alt_l, // 显示日志 shortcut_alt_p, // 显示捕获进度 shortcut_alt_c, // 清空缓存 ]; for (const shortcut of shortcuts) { window.addEventListener("keydown", shortcut, true); } } function later_main() { show_tips(); set_shortcuts(); } function main() { early_main(); document.addEventListener("DOMContentLoaded", later_main); } main(); })();