Greasy Fork is available in English.
可知网导出页面到PDF,仅对PDF预览有效
ของเมื่อวันที่
- // ==UserScript==// @name keledge-helper// @namespace http://tampermonkey.net/// @version 0.1// @description 可知网导出页面到PDF,仅对PDF预览有效// @author 2690874578@qq.com// @match https://www.keledge.com/pdfReader?*// @require https://cdn.staticfile.org/pdf-lib/1.17.1/pdf-lib.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>`;// 全局变量window.pdf_data_list = [];window.log = console.log.bind(console);window.error = console.error.bind(console);/*** @param {number} delay*/function sleep(delay) {return new Promise(resolve => setTimeout(resolve, delay));}async function wait_for_pdfjs() {while (!window.pdfjsLib) {await sleep(200);}}function hooked_get_doc(pdf_data) {pdf_data_list.push(pdf_data.data);log(`page collected: ${pdf_data_list.length}`);return getDocument(pdf_data);}function hook_pdfjs() {window.getDocument = pdfjsLib.getDocument.bind(pdfjsLib);pdfjsLib.getDocument = hooked_get_doc;}/*** 加载CDN脚本* @param {string} url*/async function load_web_script(url) {try {// xhr+eval方式const resp = await fetch(url);const code = await resp.text();Function(code)();} catch(e) {error(e);// 嵌入<script>方式return new Promise((resolve) => {const script = document.createElement("script");script.src = url;script.onload = resolve;document.body.append(script);});}}/*** 返回一个包含计数器的迭代器, 其每次迭代值为 [index, value]* @param {Iterable} iterable* @returns*/function* enumerate(iterable) {let i = 0;for (let value of iterable) {yield [i, value];i++;}}/*** 合并多个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";await load_web_script(url);}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);}async function export_pdf() {const combined = await join_pdfs(pdf_data_list);save(document.title + ".pdf", combined, "application/pdf");}/*** @param {string} selectors* @returns {HTMLElement}*/function $(selectors) {const self = this?.querySelector ? this : document;return self.querySelector(selectors);}/*** 等待直到函数返回true* @param {Function} is_ready 判断条件达成与否的函数* @param {number} timeout 最大等待秒数, 默认5000毫秒* @returns {Promise<boolean>} 是否在超时前返回*/async function until(is_ready, timeout=5000) {const gap = 200;let chances = parseInt(timeout / gap);chances = chances < 1 ? 1 : chances;while (!is_ready()) {await sleep(200);chances -= 1;if (!chances) {break;}}if (chances === 0) {error(`超时!(${timeout} ms);超时函数: `, is_ready);return false;}return true;}/*** 判断指定页码的页面是否加载完成* @param {number} page_no* @returns*/function is_page_loaded(page_no) {return !!$(`[id*="pdf-page-${page_no}"] [data-loaded="true"]`);}/*** @param {HTMLElement} element* @returns {Promise<boolean>} 是否被封禁*/async function on_page_loaded(element) {const success = await until(() => $.call(element, `[data-loaded="true"]`));return !success;}async function main() {log("进入 keledge-helper 脚本");await wait_for_pdfjs();hook_pdfjs();window.btn1_fn = export_pdf;const gui = GUI.replace("{{btn1_desc}}", "导出PDF");document.body.insertAdjacentHTML("beforeend", gui);}main();})();