Greasy Fork is available in English.

keledge-helper

可知网导出页面到PDF,仅对PDF预览有效

  1. // ==UserScript==
  2. // @name keledge-helper
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.2
  5. // @description 可知网导出页面到PDF,仅对PDF预览有效
  6. // @author 2690874578@qq.com
  7. // @match https://www.keledge.com/pdfReader?*
  8. // @require https://cdn.staticfile.net/pdf-lib/1.17.1/pdf-lib.min.js
  9. // @require https://cdn.staticfile.net/sweetalert2/11.10.3/sweetalert2.all.min.js
  10. // @icon https://www.google.com/s2/favicons?sz=64&domain=keledge.com
  11. // @grant none
  12. // @run-at document-start
  13. // @license GPL-3.0-only
  14. // ==/UserScript==
  15.  
  16.  
  17. (function () {
  18. "use strict";
  19.  
  20. // 全局常量
  21. 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>`;
  22. const pdf_data_map = new Map();
  23. const println = console.log.bind(console);
  24. const logs = [];
  25.  
  26. // 全局变量
  27. let page_index = -1;
  28.  
  29. // 全局属性
  30. Object.assign(window, { println, pdf_data_map });
  31.  
  32. function log(...args) {
  33. const time = new Date().toTimeString().split(" ")[0];
  34. const record = `[${time}]\t${args}`;
  35. logs.push(record);
  36. println(...args);
  37. }
  38.  
  39. function clear_pdf_data() {
  40. const size = pdf_data_map.size;
  41. pdf_data_map.clear();
  42. log(`PDF缓存已清空,共清理 ${size} 页`);
  43. }
  44.  
  45. /**
  46. * @param {number} delay
  47. */
  48. function sleep(delay) {
  49. return new Promise((resolve) => setTimeout(resolve, delay));
  50. }
  51.  
  52. /**
  53. * @param {string[]} libs
  54. */
  55. async function wait_for_libs(libs) {
  56. let not_ready = true;
  57. while (not_ready) {
  58. for (const lib of libs) {
  59. if (!window[lib]) {
  60. not_ready = true;
  61. break;
  62. } else {
  63. not_ready = false;
  64. }
  65. }
  66. await sleep(200);
  67. }
  68. }
  69.  
  70. /**
  71. * 替换 window.glob_obj_name.method 为 new_method
  72. * @param {string} glob_obj_name
  73. * @param {string} method
  74. * @param {Function} new_method
  75. */
  76. function hook_method(glob_obj_name, method, new_method) {
  77. const obj = window[glob_obj_name];
  78. window[method] = obj[method].bind(obj);
  79. window["_" + glob_obj_name] = obj;
  80.  
  81. window[glob_obj_name] = new Proxy(obj, {
  82. get(target, property, _) {
  83. if (property === method) {
  84. println(
  85. `代理并替换了 ${glob_obj_name}.${property} 属性(方法)访问`
  86. );
  87. return new_method;
  88. }
  89. return target[property];
  90. },
  91. });
  92. }
  93.  
  94. function hooked_get_doc(pdf_data) {
  95. // debugger;
  96. if (!pdf_data_map.has(page_index)) {
  97. pdf_data_map.set(page_index, pdf_data.data);
  98. log(`已经捕获数量:${pdf_data_map.size}`);
  99. }
  100. return window["getDocument"](pdf_data);
  101. }
  102.  
  103. function hook_pdfjs() {
  104. hook_method("pdfjsLib", "getDocument", hooked_get_doc);
  105. }
  106.  
  107. /**
  108. * @param {{ id: string, container: HTMLDivElement, eventBus: any, "110n": any, linkService: any, textLayerMode: number }} config
  109. */
  110. function hooked_viewer(config) {
  111. // id: "pdf-page-0"
  112. page_index = parseInt(config.id.split("-").at(-1));
  113. log(`正在加载页面:${page_index + 1}`);
  114. return new window["PDFViewer"](config);
  115. }
  116.  
  117. function hook_viewer() {
  118. hook_method("pdfjsViewer", "PDFViewer", hooked_viewer);
  119. }
  120.  
  121. /**
  122. * 返回一个包含计数器的迭代器, 其每次迭代值为 [index, value]
  123. * @param {Iterable} iterable
  124. * @returns
  125. */
  126. function* enumerate(iterable) {
  127. let i = 0;
  128. for (let value of iterable) {
  129. yield [i, value];
  130. i++;
  131. }
  132. }
  133.  
  134. async function myalert(text) {
  135. return Sweetalert2.fire({
  136. text,
  137. icon: "error",
  138. allowOutsideClick: false,
  139. });
  140. }
  141.  
  142. /**
  143. * 合并多个PDF
  144. * @param {Array<ArrayBuffer | Uint8Array>} pdfs
  145. * @returns {Promise<Uint8Array>}
  146. */
  147. async function join_pdfs(pdfs) {
  148. if (!window.PDFLib) {
  149. const url =
  150. "https://cdn.staticfile.org/pdf-lib/1.17.1/pdf-lib.min.js";
  151. const code = await fetch(url).then((resp) => resp.text());
  152. eval(code);
  153. }
  154.  
  155. if (!window.PDFLib) {
  156. const msg = "缺少 PDFLib 无法导出 PDF!";
  157. myalert(msg);
  158. throw new Error(msg);
  159. }
  160.  
  161. const combined = await PDFLib.PDFDocument.create();
  162.  
  163. for (const [i, buffer] of enumerate(pdfs)) {
  164. const pdf = await PDFLib.PDFDocument.load(buffer);
  165. const pages = await combined.copyPages(pdf, pdf.getPageIndices());
  166.  
  167. for (const page of pages) {
  168. combined.addPage(page);
  169. }
  170. log(`已经合并 ${i + 1} 组`);
  171. }
  172.  
  173. return combined.save();
  174. }
  175.  
  176. /**
  177. * 创建并下载文件
  178. * @param {string} file_name 文件名
  179. * @param {ArrayBuffer | ArrayBufferView | Blob | string} content 内容
  180. * @param {string} type 媒体类型,需要符合 MIME 标准
  181. */
  182. function save(file_name, content, type = "") {
  183. const blob = new Blob([content], { type });
  184. const size = (blob.size / 1024).toFixed(1);
  185. log(`blob saved, size: ${size} kb, type: ${blob.type}`);
  186.  
  187. const url = URL.createObjectURL(blob);
  188. const a = document.createElement("a");
  189. a.download = file_name || "未命名文件";
  190. a.href = url;
  191. a.click();
  192. URL.revokeObjectURL(url);
  193. }
  194.  
  195. /**
  196. * @param {string} text
  197. * @returns {Promise<boolean>}
  198. */
  199. async function myconfirm(text) {
  200. const result = await Sweetalert2.fire({
  201. text,
  202. icon: "warning",
  203. showCancelButton: true,
  204. confirmButtonColor: "#3085d6",
  205. cancelButtonColor: "#d33",
  206. allowOutsideClick: false,
  207. });
  208. return result.isConfirmed;
  209. }
  210.  
  211. async function export_pdf() {
  212. const yes = await myconfirm("是否导出已经捕获的页面?导出后会清空缓存");
  213. if (!yes) {
  214. return;
  215. }
  216.  
  217. // 每个 Item 是 [页码, 数据]
  218. const pdfs = Array.from(pdf_data_map)
  219. .sort((a, b) => a[0] - b[0])
  220. .map((item) => item[1]);
  221.  
  222. const combined = await join_pdfs(pdfs);
  223. save(document.title + ".pdf", combined, "application/pdf");
  224. clear_pdf_data();
  225. }
  226.  
  227. function show_tips() {
  228. Sweetalert2.fire({
  229. title: "可知助手小提示",
  230. html: "<p>以下快捷键可用: </p><p>显示帮助: ALT + H</p><p>导出文档: ALT + S</p><p>显示日志: ALT + L</p><p>进度明细: ALT + P</p><p>清空缓存: ALT + C</p>",
  231. timer: 10000,
  232. timerProgressBar: true,
  233. allowOutsideClick: true,
  234. });
  235. }
  236.  
  237. /**
  238. * 按下 alt + h 弹出帮助文档
  239. * @param {KeyboardEvent} event
  240. */
  241. function shortcut_alt_h(event) {
  242. if (!(event.altKey && event.code === "KeyH")) {
  243. return;
  244. }
  245. show_tips();
  246. }
  247.  
  248. /**
  249. * 按下 alt + s 以导出PDF
  250. * @param {KeyboardEvent} event
  251. */
  252. function shortcut_alt_s(event) {
  253. if (!(event.altKey && event.code === "KeyS")) {
  254. return;
  255. }
  256. export_pdf();
  257. }
  258.  
  259. /**
  260. * 按下 alt + l 以显示日志
  261. * @param {KeyboardEvent} event
  262. */
  263. function shortcut_alt_l(event) {
  264. if (!(event.altKey && event.code === "KeyL")) {
  265. return;
  266. }
  267. const text = logs.join("\n");
  268. Sweetalert2.fire({
  269. title: "可知助手日志",
  270. html: `<textarea readonly rows="10" cols="50" style="resize: none;">${text}</textarea>`,
  271. showConfirmButton: false,
  272. });
  273. }
  274.  
  275. /**
  276. * 描述整数数组
  277. * @param {number[]} nums
  278. * @returns {string}
  279. */
  280. function desc_num_arr(nums) {
  281. const result = [];
  282. let start = null;
  283. let end = null;
  284.  
  285. for (let i = 0; i < nums.length; i++) {
  286. if (start === null) {
  287. start = nums[i];
  288. end = nums[i];
  289. } else if (nums[i] === end + 1) {
  290. end = nums[i];
  291. } else {
  292. if (start === end) {
  293. result.push(`${start}`);
  294. } else {
  295. result.push(`${start}-${end}`);
  296. }
  297. start = nums[i];
  298. end = nums[i];
  299. }
  300. }
  301.  
  302. if (start !== null) {
  303. if (start === end) {
  304. result.push(start.toString());
  305. } else {
  306. result.push(`${start}-${end}`);
  307. }
  308. }
  309.  
  310. return result.join(", ");
  311. }
  312.  
  313. /**
  314. * 按下 alt + p 以显示进度详情
  315. * @param {KeyboardEvent} event
  316. */
  317. function shortcut_alt_p(event) {
  318. if (!(event.altKey && event.code === "KeyP")) {
  319. return;
  320. }
  321.  
  322. const captured = Array
  323. .from(pdf_data_map.keys())
  324. .sort((a, b) => a - b)
  325. .map(pn => pn + 1);
  326. const progress = desc_num_arr(captured);
  327.  
  328. Sweetalert2.fire({
  329. title: "页面捕获进度",
  330. text: captured.length ? `已经捕获的页码:${progress}` : `尚未捕获任何页面`,
  331. });
  332. }
  333.  
  334. /**
  335. * 按下 alt + c 以显示进度详情
  336. * @param {KeyboardEvent} event
  337. */
  338. async function shortcut_alt_c(event) {
  339. if (!(event.altKey && event.code === "KeyC")) {
  340. return;
  341. }
  342.  
  343. const hint = `是否清空所有已经捕获的页面(共 ${pdf_data_map.size} 页)?`;
  344. const yes = await myconfirm(hint);
  345. if (!yes) {
  346. return;
  347. }
  348.  
  349. clear_pdf_data();
  350. Sweetalert2.fire({
  351. icon: "info",
  352. text: "缓存已清空",
  353. });
  354. }
  355.  
  356. async function early_main() {
  357. log("进入 keledge-helper 脚本");
  358.  
  359. await wait_for_libs(["pdfjsLib", "pdfjsViewer"]);
  360. hook_viewer();
  361. hook_pdfjs();
  362.  
  363. window.btn1_fn = export_pdf;
  364. const gui = GUI.replace("{{btn1_desc}}", "导出PDF");
  365. document.body.insertAdjacentHTML("beforeend", gui);
  366. }
  367.  
  368. function set_shortcuts() {
  369. const shortcuts = [
  370. shortcut_alt_h, // 显示帮助
  371. shortcut_alt_s, // 导出pdf
  372. shortcut_alt_l, // 显示日志
  373. shortcut_alt_p, // 显示捕获进度
  374. shortcut_alt_c, // 清空缓存
  375. ];
  376.  
  377. for (const shortcut of shortcuts) {
  378. window.addEventListener("keydown", shortcut, true);
  379. }
  380. }
  381.  
  382. function later_main() {
  383. show_tips();
  384. set_shortcuts();
  385. }
  386.  
  387. function main() {
  388. early_main();
  389. document.addEventListener("DOMContentLoaded", later_main);
  390. }
  391.  
  392. main();
  393. })();