Wenku Doc Downloader

对文档截图,合并为纯图片PDF。有限地支持(1)豆丁网(2)道客巴巴(3)360个人图书馆(4)得力文库(5)MBA智库(6)爱问文库(7)原创力文档(8)读根网(9)国标网(10)安全文库网(11)人人文库(12)云展网(13)360文库(14)技工教育网(15)文库吧(16)中国社会科学文库(17)金锄头(18)自然资源标准。预览多少页,导出多少页。额外支持(1)食典通(2)JJG 计量技术规范,详见下方说明。

当前为 2024-02-23 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Wenku Doc Downloader
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.10.0
  5. // @description 对文档截图,合并为纯图片PDF。有限地支持(1)豆丁网(2)道客巴巴(3)360个人图书馆(4)得力文库(5)MBA智库(6)爱问文库(7)原创力文档(8)读根网(9)国标网(10)安全文库网(11)人人文库(12)云展网(13)360文库(14)技工教育网(15)文库吧(16)中国社会科学文库(17)金锄头(18)自然资源标准。预览多少页,导出多少页。额外支持(1)食典通(2)JJG 计量技术规范,详见下方说明。
  6. // @author 2690874578@qq.com
  7. // @match *://*.docin.com/p-*
  8. // @match *://docimg1.docin.com/?wk=true
  9. // @match *://ishare.iask.sina.com.cn/f/*
  10. // @match *://ishare.iask.com/f/*
  11. // @match *://swf.ishare.down.sina.com.cn/?path=*
  12. // @match *://swf.ishare.down.sina.com.cn/?wk=true
  13. // @match *://www.deliwenku.com/p-*
  14. // @match *://file.deliwenku.com/?num=*
  15. // @match *://file3.deliwenku.com/?num=*
  16. // @match *://www.doc88.com/p-*
  17. // @match *://www.360doc.com/content/*
  18. // @match *://doc.mbalib.com/view/*
  19. // @match *://www.dugen.com/p-*
  20. // @match *://max.book118.com/html/*
  21. // @match *://openapi.book118.com/?*
  22. // @match *://view-cache.book118.com/pptView.html?*
  23. // @match *://*.book118.com/?readpage=*
  24. // @match *://c.gb688.cn/bzgk/gb/showGb?*
  25. // @match *://www.safewk.com/p-*
  26. // @match *://www.renrendoc.com/paper/*
  27. // @match *://www.renrendoc.com/p-*
  28. // @match *://www.yunzhan365.com/basic/*
  29. // @match *://book.yunzhan365.com/*index.html*
  30. // @match *://wenku.so.com/d/*
  31. // @match *://jg.class.com.cn/cms/resourcedetail.htm?contentUid=*
  32. // @match *://preview.imm.aliyuncs.com/index.html?url=*/jgjyw/*
  33. // @match *://www.wenkub.com/p-*.html*
  34. // @match *://*/manuscripts/?*
  35. // @match *://gwfw.sdlib.com:8000/*
  36. // @match *://www.jinchutou.com/shtml/view-*
  37. // @match *://www.jinchutou.com/p-*
  38. // @match *://www.nrsis.org.cn/*/read/*
  39. // @match https://xianxiao.ssap.com.cn/index/rpdf/read/id/*/catalog_id/0.html?file=*
  40. // @require https://cdn.staticfile.org/jspdf/2.5.1/jspdf.umd.min.js
  41. // @require https://cdn.staticfile.org/html2canvas/1.4.1/html2canvas.min.js
  42. // @icon https://s2.loli.net/2022/01/12/wc9je8RX7HELbYQ.png
  43. // @icon64 https://s2.loli.net/2022/01/12/tmFeSKDf8UkNMjC.png
  44. // @grant none
  45. // @run-at document-idle
  46. // @license GPL-3.0-only
  47. // @create 2021-11-22
  48. // @note 1. 新增支持【先晓书院】
  49. // ==/UserScript==
  50.  
  51.  
  52. (function () {
  53. 'use strict';
  54.  
  55. /**
  56. * 基于 window.postMessage 通信的套接字对象
  57. */
  58. class Socket {
  59. /**
  60. * 创建套接字对象
  61. * @param {Window} target 目标窗口
  62. */
  63. constructor(target) {
  64. if (!(target.window && (target === target.window))) {
  65. console.log(target);
  66. throw new Error(`target is not a [Window Object]`);
  67. }
  68. this.target = target;
  69. this.connected = false;
  70. this.listeners = new Set();
  71. }
  72.  
  73. get [Symbol.toStringTag]() { return "Socket"; }
  74.  
  75. /**
  76. * 向目标窗口发消息
  77. * @param {*} message
  78. */
  79. talk(message) {
  80. if (!this.target) {
  81. throw new TypeError(
  82. `socket.target is not a window: ${this.target}`
  83. );
  84. }
  85. this.target.postMessage(message, "*");
  86. }
  87.  
  88. /**
  89. * 添加捕获型监听器,返回实际添加的监听器
  90. * @param {Function} listener (e: MessageEvent) => {...}
  91. * @param {boolean} once 是否在执行后自动销毁,默认 false;如为 true 则使用自动包装过的监听器
  92. * @returns {Function} listener
  93. */
  94. listen(listener, once=false) {
  95. if (this.listeners.has(listener)) {
  96. return;
  97. }
  98.  
  99. let real_listener = listener;
  100. // 包装监听器
  101. if (once) {
  102. const self = this;
  103. function wrapped(e) {
  104. listener(e);
  105. self.notListen(wrapped);
  106. }
  107. real_listener = wrapped;
  108. }
  109. // 添加监听器
  110. this.listeners.add(real_listener);
  111. window.addEventListener(
  112. "message", real_listener, true
  113. );
  114. return real_listener;
  115. }
  116.  
  117. /**
  118. * 移除socket上的捕获型监听器
  119. * @param {Function} listener (e: MessageEvent) => {...}
  120. */
  121. notListen(listener) {
  122. console.log(listener);
  123. console.log(
  124. "listener delete operation:",
  125. this.listeners.delete(listener)
  126. );
  127. window.removeEventListener("message", listener, true);
  128. }
  129.  
  130. /**
  131. * 检查对方来信是否为pong消息
  132. * @param {MessageEvent} e
  133. * @param {Function} resolve
  134. */
  135. _on_pong(e, resolve) {
  136. // 收到pong消息
  137. if (e.data.pong) {
  138. this.connected = true;
  139. this.listeners.forEach(
  140. listener => listener.ping ? this.notListen(listener) : 0
  141. );
  142. console.log("Client: Connected!\n" + new Date());
  143. resolve(this);
  144. }
  145. }
  146.  
  147. /**
  148. * 向对方发送ping消息
  149. * @returns {Promise<Socket>}
  150. */
  151. _ping() {
  152. return new Promise((resolve, reject) => {
  153. // 绑定pong检查监听器
  154. const listener = this.listen(
  155. e => this._on_pong(e, resolve)
  156. );
  157. listener.ping = true;
  158.  
  159. // 5分钟后超时
  160. setTimeout(
  161. () => reject(new Error(`Timeout Error during receiving pong (>5min)`)),
  162. 5 * 60 * 1000
  163. );
  164. // 发送ping消息
  165. this.talk({ ping: true });
  166. });
  167. }
  168.  
  169. /**
  170. * 检查对方来信是否为ping消息
  171. * @param {MessageEvent} e
  172. * @param {Function} resolve
  173. */
  174. _on_ping(e, resolve) {
  175. // 收到ping消息
  176. if (e.data.ping) {
  177. this.target = e.source;
  178. this.connected = true;
  179. this.listeners.forEach(
  180. listener => listener.pong ? this.notListen(listener) : 0
  181. );
  182. console.log("Server: Connected!\n" + new Date());
  183. // resolve 后期约状态无法回退
  184. // 但后续代码仍可执行
  185. resolve(this);
  186. // 回应pong消息
  187. this.talk({ pong: true });
  188. }
  189. }
  190.  
  191. /**
  192. * 当对方来信是为ping消息时回应pong消息
  193. * @returns {Promise<Socket>}
  194. */
  195. _pong() {
  196. return new Promise(resolve => {
  197. // 绑定ping检查监听器
  198. const listener = this.listen(
  199. e => this._on_ping(e, resolve)
  200. );
  201. listener.pong = true;
  202. });
  203. }
  204.  
  205. /**
  206. * 连接至目标窗口
  207. * @param {boolean} talk_first 是否先发送ping消息
  208. * @param {Window} target 目标窗口
  209. * @returns {Promise<Socket>}
  210. */
  211. connect(talk_first) {
  212. // 先发起握手
  213. if (talk_first) {
  214. return this._ping();
  215. }
  216. // 后发起握手
  217. return this._pong();
  218. }
  219. }
  220.  
  221.  
  222. const base = {
  223. Socket,
  224.  
  225. init_gbk_encoder() {
  226.  
  227. let table;
  228.  
  229. function initGbkTable() {
  230. // https://en.wikipedia.org/wiki/GBK_(character_encoding)#Encoding
  231. const ranges = [
  232. [0xA1, 0xA9, 0xA1, 0xFE],
  233. [0xB0, 0xF7, 0xA1, 0xFE],
  234. [0x81, 0xA0, 0x40, 0xFE],
  235. [0xAA, 0xFE, 0x40, 0xA0],
  236. [0xA8, 0xA9, 0x40, 0xA0],
  237. [0xAA, 0xAF, 0xA1, 0xFE],
  238. [0xF8, 0xFE, 0xA1, 0xFE],
  239. [0xA1, 0xA7, 0x40, 0xA0],
  240. ];
  241. const codes = new Uint16Array(23940);
  242. let i = 0;
  243.  
  244. for (const [b1Begin, b1End, b2Begin, b2End] of ranges) {
  245. for (let b2 = b2Begin; b2 <= b2End; b2++) {
  246. if (b2 !== 0x7F) {
  247. for (let b1 = b1Begin; b1 <= b1End; b1++) {
  248. codes[i++] = b2 << 8 | b1;
  249. }
  250. }
  251. }
  252. }
  253. table = new Uint16Array(65536);
  254. table.fill(0xFFFF);
  255.  
  256. const str = new TextDecoder('gbk').decode(codes);
  257. for (let i = 0; i < str.length; i++) {
  258. table[str.charCodeAt(i)] = codes[i];
  259. }
  260. }
  261.  
  262. const defaultOnAlloc = (len) => new Uint8Array(len);
  263. const defaultOnError = () => 63; // '?'
  264.  
  265. /**
  266. * 字符串编码为gbk字节串
  267. * @param {string} str
  268. * @param {Function} onError 处理编码失败时返回字符替代值的函数,默认是返回 63('?') 的函数
  269. * @returns {Uint8Array}
  270. */
  271. return function(str, onError=null) {
  272. if (!table) {
  273. initGbkTable();
  274. }
  275. const onAlloc = defaultOnAlloc;
  276. onError = onError === null ? defaultOnError : onError;
  277.  
  278. const buf = onAlloc(str.length * 2);
  279. let n = 0;
  280.  
  281. for (let i = 0; i < str.length; i++) {
  282. const code = str.charCodeAt(i);
  283. if (code < 0x80) {
  284. buf[n++] = code;
  285. continue;
  286. }
  287.  
  288. const gbk = table[code];
  289.  
  290. if (gbk !== 0xFFFF) {
  291. buf[n++] = gbk;
  292. buf[n++] = gbk >> 8;
  293. }
  294. else if (code === 8364) {
  295. // 8364 == '€'.charCodeAt(0)
  296. // Code Page 936 has a single-byte euro sign at 0x80
  297. buf[n++] = 0x80;
  298. }
  299. else {
  300. const ret = onError(i, str);
  301. if (ret === -1) {
  302. break;
  303. }
  304. if (ret > 0xFF) {
  305. buf[n++] = ret;
  306. buf[n++] = ret >> 8;
  307. } else {
  308. buf[n++] = ret;
  309. }
  310. }
  311. }
  312. return buf.subarray(0, n)
  313. }
  314. },
  315.  
  316. /**
  317. * Construct a table with table[i] as the length of the longest prefix of the substring 0..i
  318. * @param {Array<number>} arr
  319. * @returns {Array<number>}
  320. */
  321. longest_prefix: function(arr) {
  322.  
  323. // create a table of size equal to the length of `str`
  324. // table[i] will store the prefix of the longest prefix of the substring str[0..i]
  325. let table = new Array(arr.length);
  326. let maxPrefix = 0;
  327. // the longest prefix of the substring str[0] has length
  328. table[0] = 0;
  329.  
  330. // for the substrings the following substrings, we have two cases
  331. for (let i = 1; i < arr.length; i++) {
  332. // case 1. the current character doesn't match the last character of the longest prefix
  333. while (maxPrefix > 0 && arr[i] !== arr[maxPrefix]) {
  334. // if that is the case, we have to backtrack, and try find a character that will be equal to the current character
  335. // if we reach 0, then we couldn't find a chracter
  336. maxPrefix = table[maxPrefix - 1];
  337. }
  338. // case 2. The last character of the longest prefix matches the current character in `str`
  339. if (arr[maxPrefix] === arr[i]) {
  340. // if that is the case, we know that the longest prefix at position i has one more character.
  341. // for example consider `-` be any character not contained in the set [a-c]
  342. // str = abc----abc
  343. // consider `i` to be the last character `c` in `str`
  344. // maxPrefix = will be 2 (the first `c` in `str`)
  345. // maxPrefix now will be 3
  346. maxPrefix++;
  347. // so the max prefix for table[9] is 3
  348. }
  349. table[i] = maxPrefix;
  350. }
  351. return table;
  352. },
  353.  
  354. // 用于取得一次列表中所有迭代器的值
  355. getAllValus: function(iterators) {
  356. if (iterators.length === 0) {
  357. return [true, []];
  358. }
  359. let values = [];
  360. for (let iterator of iterators) {
  361. let {value, done} = iterator.next();
  362. if (done) {
  363. return [true, []];
  364. }
  365. values.push(value);
  366. }
  367. return [false, values];
  368. },
  369.  
  370. /**
  371. * 使用过时的execCommand复制文字
  372. * @param {string} text
  373. */
  374. oldCopy: function(text) {
  375. document.oncopy = function(event) {
  376. event.clipboardData.setData('text/plain', text);
  377. event.preventDefault();
  378. };
  379. document.execCommand('Copy', false, null);
  380. },
  381.  
  382. b64ToUint6: function(nChr) {
  383. return nChr > 64 && nChr < 91 ?
  384. nChr - 65
  385. : nChr > 96 && nChr < 123 ?
  386. nChr - 71
  387. : nChr > 47 && nChr < 58 ?
  388. nChr + 4
  389. : nChr === 43 ?
  390. 62
  391. : nChr === 47 ?
  392. 63
  393. :
  394. 0;
  395. },
  396.  
  397. /**
  398. * 元素选择器
  399. * @param {string} selector 选择器
  400. * @returns {Array<HTMLElement>} 元素列表
  401. */
  402. $: function(selector) {
  403. const self = this?.querySelectorAll ? this : document;
  404. return [...self.querySelectorAll(selector)];
  405. },
  406.  
  407. /**
  408. * 安全元素选择器,直到元素存在时才返回元素列表,最多等待5秒
  409. * @param {string} selector 选择器
  410. * @returns {Promise<Array<HTMLElement>>} 元素列表
  411. */
  412. $$: async function(selector) {
  413. const self = this?.querySelectorAll ? this : document;
  414.  
  415. for (let i = 0; i < 10; i++) {
  416. let elems = [...self.querySelectorAll(selector)];
  417. if (elems.length > 0) {
  418. return elems;
  419. }
  420. await new Promise(r => setTimeout(r, 500));
  421. }
  422. throw Error(`"${selector}" not found in 5s`);
  423. },
  424.  
  425. /**
  426. * 将2个及以上的空白字符(除了换行符)替换成一个空格
  427. * @param {string} text
  428. * @returns {string}
  429. */
  430. stripBlanks: function(text) {
  431. return text
  432. .replace(/([^\r\n])(\s{2,})(?=[^\r\n])/g, "$1 ")
  433. .replace(/\n{2,}/, "\n");
  434. },
  435.  
  436. /**
  437. * 复制属性(含访问器)到 target
  438. * @param {Object} target
  439. * @param {...Object} sources
  440. * @returns
  441. */
  442. superAssign: function(target, ...sources) {
  443. sources.forEach(source =>
  444. Object.defineProperties(
  445. target, Object.getOwnPropertyDescriptors(source)
  446. )
  447. );
  448. return target;
  449. },
  450.  
  451. makeCRC32: function() {
  452. function makeCRCTable() {
  453. let c;
  454. let crcTable = [];
  455. for(var n =0; n < 256; n++){
  456. c = n;
  457. for(var k =0; k < 8; k++){
  458. c = ((c&1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
  459. }
  460. crcTable[n] = c;
  461. }
  462. return crcTable;
  463. }
  464.  
  465. const crcTable = makeCRCTable();
  466.  
  467. /**
  468. * @param {string} str
  469. * @returns {number}
  470. */
  471. return function(str) {
  472. let crc = 0 ^ (-1);
  473. for (var i = 0; i < str.length; i++ ) {
  474. crc = (crc >>> 8) ^ crcTable[(crc ^ str.charCodeAt(i)) & 0xFF];
  475. }
  476. return (crc ^ (-1)) >>> 0;
  477. };
  478. }
  479. };
  480.  
  481. const box = `
  482. <div class="wk-box">
  483. <section class="btns-sec">
  484. <p class="logo_tit">Wenku Doc Downloader</p>
  485. <button class="btn-1">展开文档 😈</button>
  486. <button class="btn-2">空按钮 2</button>
  487. <button class="btn-3">空按钮 3</button>
  488. <button class="btn-4">空按钮 4</button>
  489. <button class="btn-5">空按钮 5</button>
  490. </section>
  491. <p class="wk-fold-btn unfold"></p>
  492. </div>
  493. `;
  494.  
  495. const style = `
  496. <style class="wk-style">
  497. .wk-fold-btn {
  498. position: fixed;
  499. left: 151px;
  500. top: 36%;
  501. user-select: none;
  502. font-size: large;
  503. z-index: 1001;
  504. }
  505.  
  506. .wk-fold-btn::after {
  507. content: "🐵";
  508. }
  509. .wk-fold-btn.folded {
  510. left: 20px;
  511. }
  512. .wk-fold-btn.folded::after {
  513. content: "🙈";
  514. }
  515.  
  516. .wk-box {
  517. position: fixed;
  518. width: 154px;
  519. left: 10px;
  520. top: 32%;
  521. z-index: 1000;
  522. }
  523.  
  524. .btns-sec {
  525. background: #E7F1FF;
  526. border: 2px solid #1676FF;
  527. padding: 0px 0px 10px 0px;
  528. font-weight: 600;
  529. border-radius: 2px;
  530. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
  531. 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji',
  532. 'Segoe UI Emoji', 'Segoe UI Symbol';
  533. }
  534.  
  535. .btns-sec.folded {
  536. display: none;
  537. }
  538.  
  539. .logo_tit {
  540. width: 100%;
  541. background: #1676FF;
  542. text-align: center;
  543. font-size: 12px;
  544. color: #E7F1FF;
  545. line-height: 40px;
  546. height: 40px;
  547. margin: 0 0 16px 0;
  548. }
  549.  
  550. .btn-1 {
  551. display: block;
  552. width: 128px;
  553. height: 28px;
  554. background: linear-gradient(180deg, #00E7F7 0%, #FEB800 0.01%, #FF8700 100%);
  555. border-radius: 4px;
  556. color: #fff;
  557. font-size: 12px;
  558. border: none;
  559. outline: none;
  560. margin: 8px auto;
  561. font-weight: bold;
  562. cursor: pointer;
  563. opacity: .9;
  564. }
  565.  
  566. .btn-2 {
  567. display: none;
  568. width: 128px;
  569. height: 28px;
  570. background: #07C160;
  571. border-radius: 4px;
  572. color: #fff;
  573. font-size: 12px;
  574. border: none;
  575. outline: none;
  576. margin: 8px auto;
  577. font-weight: bold;
  578. cursor: pointer;
  579. opacity: .9;
  580. }
  581.  
  582. .btn-3 {
  583. display: none;
  584. width: 128px;
  585. height: 28px;
  586. background: #FA5151;
  587. border-radius: 4px;
  588. color: #fff;
  589. font-size: 12px;
  590. border: none;
  591. outline: none;
  592. margin: 8px auto;
  593. font-weight: bold;
  594. cursor: pointer;
  595. opacity: .9;
  596. }
  597.  
  598. .btn-4 {
  599. display: none;
  600. width: 128px;
  601. height: 28px;
  602. background: #1676FF;
  603. border-radius: 4px;
  604. color: #fff;
  605. font-size: 12px;
  606. border: none;
  607. outline: none;
  608. margin: 8px auto;
  609. font-weight: bold;
  610. cursor: pointer;
  611. opacity: .9;
  612. }
  613.  
  614. .btn-5 {
  615. display: none;
  616. width: 128px;
  617. height: 28px;
  618. background: #ff6600;
  619. border-radius: 4px;
  620. color: #fff;
  621. font-size: 12px;
  622. border: none;
  623. outline: none;
  624. margin: 8px auto;
  625. font-weight: bold;
  626. cursor: pointer;
  627. opacity: .9;
  628. }
  629.  
  630.  
  631. .btns-sec button:hover {
  632. opacity: 0.8;
  633. }
  634.  
  635. .btns-sec button:active{
  636. opacity: 1;
  637. }
  638.  
  639. .btns-sec button[disabled] {
  640. cursor: not-allowed;
  641. opacity: 1;
  642. filter: grayscale(1);
  643. }
  644.  
  645. .wk-popup-container {
  646. height: 100vh;
  647. width: 100vw;
  648. display: flex;
  649. flex-direction: column;
  650. justify-content: space-around;
  651. z-index: 999;
  652. background: 0 0;
  653. }
  654.  
  655. .wk-popup-head {
  656. font-size: 1.5em;
  657. margin-bottom: 12px
  658. }
  659.  
  660. .wk-card {
  661. background: #fff;
  662. background-image: linear-gradient(48deg, #fff 0, #e5efe9 100%);
  663. border-top-right-radius: 16px;
  664. border-bottom-left-radius: 16px;
  665. box-shadow: -20px 20px 35px 1px rgba(10, 49, 86, .18);
  666. display: flex;
  667. flex-direction: column;
  668. padding: 32px;
  669. margin: 0;
  670. max-width: 400px;
  671. width: 100%
  672. }
  673.  
  674. .content-wrapper {
  675. font-size: 1.1em;
  676. margin-bottom: 44px
  677. }
  678.  
  679. .content-wrapper:last-child {
  680. margin-bottom: 0
  681. }
  682.  
  683. .wk-button {
  684. align-items: center;
  685. background: #e5efe9;
  686. border: 1px solid #5a72b5;
  687. border-radius: 4px;
  688. color: #121943;
  689. cursor: pointer;
  690. display: flex;
  691. font-size: 1em;
  692. font-weight: 700;
  693. height: 40px;
  694. justify-content: center;
  695. width: 150px
  696. }
  697.  
  698. .wk-button:focus {
  699. border: 2px solid transparent;
  700. box-shadow: 0 0 0 2px #121943;
  701. outline: solid 4px transparent
  702. }
  703.  
  704. .link {
  705. color: #121943
  706. }
  707.  
  708. .link:focus {
  709. box-shadow: 0 0 0 2px #121943
  710. }
  711.  
  712. .input-wrapper {
  713. display: flex;
  714. flex-direction: column
  715. }
  716.  
  717. .input-wrapper .label {
  718. align-items: baseline;
  719. display: flex;
  720. font-weight: 700;
  721. justify-content: space-between;
  722. margin-bottom: 8px
  723. }
  724.  
  725. .input-wrapper .optional {
  726. color: #5a72b5;
  727. font-size: .9em
  728. }
  729.  
  730. .input-wrapper .input {
  731. border: 1px solid #5a72b5;
  732. border-radius: 4px;
  733. height: 40px;
  734. padding: 8px
  735. }
  736.  
  737. .modal-header {
  738. align-items: baseline;
  739. display: flex;
  740. justify-content: space-between
  741. }
  742.  
  743. .close {
  744. background: 0 0;
  745. border: none;
  746. cursor: pointer;
  747. display: flex;
  748. height: 16px;
  749. text-decoration: none;
  750. width: 16px
  751. }
  752.  
  753. .close svg {
  754. width: 16px
  755. }
  756.  
  757. .modal-wrapper {
  758. background: rgba(0, 0, 0, .7);
  759. }
  760.  
  761. #wk-popup {
  762. opacity: 0;
  763. transition: opacity .25s ease-in-out;
  764. display: none;
  765. flex-direction: row;
  766. justify-content: space-around;
  767. }
  768.  
  769. #wk-popup:target {
  770. opacity: 1;
  771. display: flex;
  772. }
  773.  
  774. #wk-popup:target .modal-body {
  775. opacity: 1;
  776. transform: translateY(1);
  777. }
  778.  
  779. #wk-popup .modal-body {
  780. max-width: 500px;
  781. opacity: 0;
  782. transform: translateY(-3vh);
  783. transition: opacity .25s ease-in-out;
  784. width: 100%;
  785. z-index: 1
  786. }
  787.  
  788. .outside-trigger {
  789. bottom: 0;
  790. cursor: default;
  791. left: 0;
  792. position: fixed;
  793. right: 0;
  794. top: 0;
  795. }
  796. </style>
  797. `;
  798.  
  799. const popup = `
  800. <div class="wk-popup-container">
  801. <div class='modal-wrapper' id='wk-popup'>
  802. <div class='modal-body wk-card'>
  803. <div class='modal-header'>
  804. <h2 class='wk-popup-head'>下载进度条</h2>
  805. <a href='#!' role='wk-button' class='close' aria-label='close this modal'>
  806. <svg viewBox='0 0 24 24'>
  807. <path
  808. d='M24 20.188l-8.315-8.209 8.2-8.282-3.697-3.697-8.212 8.318-8.31-8.203-3.666 3.666 8.321 8.24-8.206 8.313 3.666 3.666 8.237-8.318 8.285 8.203z'>
  809. </path>
  810. </svg>
  811. </a>
  812. </div>
  813. <p class='wk-popup-body'>正在初始化内容...</p>
  814. </div>
  815. <a href='#!' class='outside-trigger'></a>
  816. </div>
  817. </div>
  818. `;
  819.  
  820. globalThis.wk$ = base.$;
  821. globalThis.wk$$ = base.$$;
  822.  
  823.  
  824. const utils = {
  825. Socket: base.Socket,
  826.  
  827. PDF_LIB_URL: "https://cdn.staticfile.org/pdf-lib/1.17.1/pdf-lib.min.js",
  828.  
  829. encode_to_gbk: base.init_gbk_encoder(),
  830.  
  831. print: function(...args) {
  832. const time = new Date().toTimeString().slice(0, 8);
  833. console.info(`[wk ${time}]`, ...args);
  834. },
  835.  
  836. /**
  837. * 字节串转b64字符串
  838. * @param {Uint8Array} bytes
  839. * @returns {Promise<string>}
  840. */
  841. bytes_to_b64: function(bytes) {
  842. return new Promise((resolve, reject) => {
  843. const reader = new FileReader();
  844. reader.onerror = () => reject(new Error("转换失败", { cause: bytes }));
  845. reader.onloadend = () => resolve(reader.result.split(",")[1]);
  846. reader.readAsDataURL(new Blob([bytes]));
  847. });
  848. },
  849.  
  850. /**
  851. * 以指定原因弹窗提示并抛出错误
  852. * @param {string} reason
  853. */
  854. raise: function(reason) {
  855. alert(reason);
  856. throw new Error(reason);
  857. },
  858.  
  859. /**
  860. * 将错误定位转为可读的字符串
  861. * @param {Error} err
  862. * @returns {string}
  863. */
  864. get_stack: function(err) {
  865. let stack = `${err.stack}`;
  866. const matches = stack.matchAll(/at .+?( [(].+[)])/g);
  867.  
  868. for (const group of matches) {
  869. stack = stack.replace(group[1], "");
  870. }
  871. return stack.trim();
  872. },
  873.  
  874. /**
  875. * 合并多个PDF
  876. * @param {Array<ArrayBuffer | Uint8Array>} pdfs
  877. * @param {Function} loop_fn
  878. * @param {Window} win
  879. * @returns {Promise<Uint8Array>}
  880. */
  881. join_pdfs: async function(pdfs, loop_fn=null, win=null) {
  882. const _win = win || window;
  883. if (!_win.PDFLib) {
  884. await this.load_web_script(this.PDF_LIB_URL);
  885. }
  886.  
  887. const combined = await PDFLib.PDFDocument.create();
  888.  
  889. for (const [i, buffer] of this.enumerate(pdfs)) {
  890. const pdf = await PDFLib.PDFDocument.load(buffer);
  891. const pages = await combined.copyPages(
  892. pdf, pdf.getPageIndices()
  893. );
  894.  
  895. for (const page of pages) {
  896. combined.addPage(page);
  897. }
  898.  
  899. if (loop_fn) {
  900. // 如有,则使用自定义钩子函数
  901. loop_fn();
  902. } else {
  903. // 否则使用旧版 popup
  904. this.update_popup(`已经合并 ${i + 1} 组`);
  905. }
  906. }
  907.  
  908. return await combined.save();
  909. },
  910.  
  911. /**
  912. * raise an error for status which is not in [200, 299]
  913. * @param {Response} response
  914. */
  915. raise_for_status(response) {
  916. if (!response.ok) {
  917. throw new Error(
  918. `Fetch Error with status code: ${response.status}`
  919. );
  920. }
  921. },
  922.  
  923. /**
  924. * 计算 str 的 CRC32 摘要(number)
  925. * @param {string} str
  926. * @returns {number}
  927. */
  928. crc32: base.makeCRC32(),
  929.  
  930. /**
  931. * 返回函数参数定义
  932. * @param {Function} fn
  933. * @param {boolean} print 是否打印到控制台,默认 true
  934. * @returns {string | undefined}
  935. */
  936. help: function(fn, print=true) {
  937. if (!(fn instanceof Function))
  938. throw new Error(`fn must be a function`);
  939.  
  940. const
  941. _fn = fn.__func__ || fn,
  942. ARROW_ARG = /^([^(]+?)=>/,
  943. FN_ARGS = /^[^(]*\(\s*([^)]*)\)/m,
  944. STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,
  945. fn_text = Function.prototype.toString.call(_fn).replace(STRIP_COMMENTS, ''),
  946. args = fn_text.match(ARROW_ARG) || fn_text.match(FN_ARGS),
  947. // 如果自带 doc,优先使用,否则使用源码
  948. doc = fn.__doc__ ? fn.__doc__ : args[0];
  949. if (!print) return base.stripBlanks(doc);
  950.  
  951. const color = (window.matchMedia &&
  952. window.matchMedia('(prefers-color-scheme: dark)').matches
  953. ) ;
  954. console.log("%c" + doc, `color: ${color}; font: small italic`);
  955. },
  956.  
  957. /**
  958. * 字节数组转十六进制字符串
  959. * @param {Uint8Array} arr
  960. * @returns {string}
  961. */
  962. hex_bytes: function(arr) {
  963. return Array.from(arr)
  964. .map(byte => byte.toString(16).padStart(2, "0"))
  965. .join("");
  966. },
  967.  
  968. /**
  969. * 取得对象类型
  970. * @param {*} obj
  971. * @returns {string} class
  972. */
  973. classof: function(obj) {
  974. return Object
  975. .prototype
  976. .toString
  977. .call(obj)
  978. .slice(8, -1);
  979. },
  980.  
  981. /**
  982. * 随机改变字体颜色、大小、粗细
  983. * @param {HTMLElement} elem
  984. */
  985. emphasize_text: function(elem) {
  986. const rand = Math.random;
  987. elem.style.cssText = `
  988. font-weight: ${200 + parseInt(700 * rand())};
  989. font-size: ${(1 + rand()).toFixed(1)}em;
  990. color: hsl(${parseInt(360 * rand())}, ${parseInt(40 + 60 * rand())}%, ${parseInt(60 * rand())}%);
  991. background-color: yellow;`;
  992. },
  993.  
  994. /**
  995. * 等待直到 DOM 节点停止变化
  996. * @param {HTMLElement} elem 监听节点
  997. * @param {number} timeout 超时毫秒数
  998. * @returns {Promise<MutationObserver>} observer
  999. */
  1000. until_stop: async function(elem, timeout=2000) {
  1001. // 创建用于共享的监听器
  1002. let observer;
  1003. // 创建超时 Promise
  1004. const timeout_promise = new Promise((_, reject) => {
  1005. setTimeout(() => {
  1006. // 停止监听、释放资源
  1007. observer.disconnect();
  1008. const error = new Error(
  1009. `Timeout Error occured on listening DOM mutation (max ${timeout}ms)`,
  1010. { cause: elem }
  1011. );
  1012. reject(error);
  1013. }, timeout);
  1014. });
  1015. // 开始元素节点变动监听
  1016. return Promise.race([
  1017. new Promise(resolve => {
  1018. // 创建监听器
  1019. observer = new MutationObserver(
  1020. (_, observer) => {
  1021. // DOM 变动结束后终止监听、释放资源
  1022. observer.disconnect();
  1023. // 返回监听器
  1024. resolve(observer);
  1025. }
  1026. );
  1027. // 开始监听目标节点
  1028. observer.observe(elem, {
  1029. subtree: true,
  1030. childList: true,
  1031. attributes: true
  1032. });
  1033. }),
  1034. timeout_promise,
  1035. ])
  1036. .catch(error => {
  1037. if (`${error}`.includes("Timeout Error")) {
  1038. return observer;
  1039. }
  1040. console.error(error);
  1041. throw error;
  1042. });
  1043. },
  1044.  
  1045. /**
  1046. * Find all the patterns that matches in a given string `str`
  1047. * this algorithm is based on the Knuth–Morris–Pratt algorithm. Its beauty consists in that it performs the matching in O(n)
  1048. * @param {Array<number>} arr
  1049. * @param {Array<number>} sub_arr
  1050. * @returns {Array<number>}
  1051. */
  1052. kmp_matching: function(arr, sub_arr) {
  1053. // find the prefix table in O(n)
  1054. let prefixes = base.longest_prefix(sub_arr);
  1055. let matches = [];
  1056.  
  1057. // `j` is the index in `P`
  1058. let j = 0;
  1059. // `i` is the index in `S`
  1060. let i = 0;
  1061. while (i < arr.length) {
  1062. // Case 1. S[i] == P[j] so we move to the next index in `S` and `P`
  1063. if (arr[i] === sub_arr[j]) {
  1064. i++;
  1065. j++;
  1066. }
  1067. // Case 2. `j` is equal to the length of `P`
  1068. // that means that we reached the end of `P` and thus we found a match
  1069. if (j === sub_arr.length) {
  1070. matches.push(i - j);
  1071. // Next we have to update `j` because we want to save some time
  1072. // instead of updating to j = 0 , we can jump to the last character of the longest prefix well known so far.
  1073. // j-1 means the last character of `P` because j is actually `P.length`
  1074. // e.g.
  1075. // S = a b a b d e
  1076. // P = `a b`a b
  1077. // we will jump to `a b` and we will compare d and a in the next iteration
  1078. // a b a b `d` e
  1079. // a b `a` b
  1080. j = prefixes[j - 1];
  1081. }
  1082. // Case 3.
  1083. // S[i] != P[j] There's a mismatch!
  1084. else if (arr[i] !== sub_arr[j]) {
  1085. // if we have found at least a character in common, do the same thing as in case 2
  1086. if (j !== 0) {
  1087. j = prefixes[j - 1];
  1088. } else {
  1089. // otherwise, j = 0, and we can move to the next character S[i+1]
  1090. i++;
  1091. }
  1092. }
  1093. }
  1094.  
  1095. return matches;
  1096. },
  1097.  
  1098. /**
  1099. * 用文件头切断文件集合体
  1100. * @param {Uint8Array} bytes
  1101. * @param {Uint8Array} head 默认 null,即使用 data 前 8 字节
  1102. * @returns {Array<Uint8Array>}
  1103. */
  1104. split_files_by_head: function(bytes, head=null) {
  1105. const sub = bytes.subarray || bytes.slice;
  1106. head = head || sub.call(bytes, 0, 8);
  1107. const indexes = this.kmp_matching(bytes, head);
  1108. const size = indexes.length;
  1109. indexes.push(bytes.length);
  1110.  
  1111. const parts = new Array(size);
  1112. for (let i = 0; i < size; i++) {
  1113. parts[i] = sub.call(bytes, indexes[i], indexes[i+1]);
  1114. }
  1115. // 返回结果数组
  1116. return parts;
  1117. },
  1118.  
  1119. /**
  1120. * 函数装饰器:仅执行一次 func
  1121. */
  1122. once: function(fn) {
  1123. let used = false;
  1124. return function() {
  1125. if (!used) {
  1126. used = true;
  1127. return fn();
  1128. }
  1129. }
  1130. },
  1131.  
  1132. /**
  1133. * 返回一个包含计数器的迭代器, 其每次迭代值为 [index, value]
  1134. * @param {Iterable} iterable
  1135. * @returns
  1136. */
  1137. enumerate: function* (iterable) {
  1138. let i = 0;
  1139. for (let value of iterable) {
  1140. yield [i, value];
  1141. i++;
  1142. }
  1143. },
  1144.  
  1145. /**
  1146. * 同步的迭代若干可迭代对象
  1147. * @param {...Iterable} iterables
  1148. * @returns
  1149. */
  1150. zip: function* (...iterables) {
  1151. // 强制转为迭代器
  1152. let iterators = iterables.map(
  1153. iterable => iterable[Symbol.iterator]()
  1154. );
  1155.  
  1156. // 逐次迭代
  1157. while (true) {
  1158. const [done, values] = base.getAllValus(iterators);
  1159. if (done) {
  1160. return;
  1161. }
  1162. if (values.length === 1) {
  1163. yield values[0];
  1164. } else {
  1165. yield values;
  1166. }
  1167. }
  1168. },
  1169.  
  1170. /**
  1171. * 返回指定范围整数生成器
  1172. * @param {number} end 如果只提供 end, 则返回 [0, end)
  1173. * @param {number} end2 如果同时提供 end2, 则返回 [end, end2)
  1174. * @param {number} step 步长, 可以为负数,不能为 0
  1175. * @returns
  1176. */
  1177. range: function*(end, end2=null, step=1) {
  1178. // 参数合法性校验
  1179. if (step === 0) {
  1180. throw new RangeError("step can't be zero");
  1181. }
  1182. const len = end2 - end;
  1183. if (end2 && len && step && (len * step < 0)) {
  1184. throw new RangeError(`[${end}, ${end2}) with step ${step} is invalid`);
  1185. }
  1186.  
  1187. // 生成范围
  1188. end2 = end2 === null ? 0 : end2;
  1189. let [small, big] = [end, end2].sort((a, b) => a - b);
  1190. // 开始迭代
  1191. if (step > 0) {
  1192. for (let i = small; i < big; i += step) {
  1193. yield i;
  1194. }
  1195. } else {
  1196. for (let i = big; i > small; i += step) {
  1197. yield i;
  1198. }
  1199. } },
  1200.  
  1201. /**
  1202. * 获取整个文档的全部css样式
  1203. * @returns {string} css text
  1204. */
  1205. get_all_styles: function() {
  1206. let styles = [];
  1207. for (let sheet of document.styleSheets) {
  1208. let rules;
  1209. try {
  1210. rules = sheet.cssRules;
  1211. } catch(e) {
  1212. if (!(e instanceof DOMException)) {
  1213. console.error(e);
  1214. }
  1215. continue;
  1216. }
  1217.  
  1218. for (let rule of rules) {
  1219. styles.push(rule.cssText);
  1220. }
  1221. }
  1222. return styles.join("\n\n");
  1223. },
  1224.  
  1225. /**
  1226. * 复制text到剪贴板
  1227. * @param {string} text
  1228. * @returns
  1229. */
  1230. copy_text: function(text) {
  1231. // 输出到控制台和剪贴板
  1232. console.log(
  1233. text.length > 20 ?
  1234. text.slice(0, 21) + "..." : text
  1235. );
  1236. if (!navigator.clipboard) {
  1237. base.oldCopy(text);
  1238. return;
  1239. }
  1240. navigator.clipboard
  1241. .writeText(text)
  1242. .catch(_ => base.oldCopy(text));
  1243. },
  1244.  
  1245. /**
  1246. * 复制媒体到剪贴板
  1247. * @param {Blob} blob
  1248. */
  1249. copy: async function(blob) {
  1250. const data = [new ClipboardItem({ [blob.type]: blob })];
  1251. try {
  1252. await navigator.clipboard.write(data);
  1253. console.log(`${blob.type} 成功复制到剪贴板`);
  1254. } catch (err) {
  1255. console.error(err.name, err.message);
  1256. }
  1257. },
  1258.  
  1259. /**
  1260. * 创建并下载文件
  1261. * @param {string} file_name 文件名
  1262. * @param {ArrayBuffer | ArrayBufferView | Blob | string} content 内容
  1263. * @param {string} type 媒体类型,需要符合 MIME 标准
  1264. */
  1265. save: function(file_name, content, type="") {
  1266. if (!type && (content instanceof Blob)) {
  1267. type = content.type;
  1268. }
  1269.  
  1270. let blob = null;
  1271. if (content instanceof Array) {
  1272. blob = new Blob(content, { type });
  1273. } else {
  1274. blob = new Blob([content], { type });
  1275. }
  1276. const size = parseInt((blob.size / 1024).toFixed(0)).toLocaleString();
  1277. console.log(`blob saved, size: ${size} KB, type: ${blob.type}`, blob);
  1278.  
  1279. const url = URL.createObjectURL(blob);
  1280. const a = document.createElement("a");
  1281. a.download = file_name || "未命名文件";
  1282. a.href = url;
  1283. a.click();
  1284. URL.revokeObjectURL(url);
  1285. },
  1286.  
  1287. /**
  1288. * 显示/隐藏按钮区
  1289. */
  1290. toggle_box: function() {
  1291. let sec = wk$(".wk-box")[0];
  1292. if (sec.style.display === "none") {
  1293. sec.style.display = "block";
  1294. return;
  1295. }
  1296. sec.style.display = "none";
  1297. },
  1298.  
  1299. /**
  1300. * 异步地睡眠 delay 毫秒, 可选 max_delay 控制波动范围
  1301. * @param {number} delay 等待毫秒
  1302. * @param {number} max_delay 最大等待毫秒, 默认为null
  1303. * @returns
  1304. */
  1305. sleep: async function(delay, max_delay=null) {
  1306. max_delay = max_delay === null ? delay : max_delay;
  1307. delay = delay + (max_delay - delay) * Math.random();
  1308. return new Promise(resolve => setTimeout(resolve, delay));
  1309. },
  1310.  
  1311. /**
  1312. * 允许打印页面
  1313. */
  1314. allow_print: function() {
  1315. const style = document.createElement("style");
  1316. style.innerHTML = `
  1317. @media print {
  1318. body { display: block; }
  1319. }`;
  1320. document.head.append(style);
  1321. },
  1322.  
  1323. /**
  1324. * 取得get参数key对应的value
  1325. * @param {string} key
  1326. * @returns {string} value
  1327. */
  1328. get_param: function(key) {
  1329. return new URL(location.href).searchParams.get(key);
  1330. },
  1331.  
  1332. /**
  1333. * 求main_set去除cut_set后的set
  1334. * @param {Iterable} main_set
  1335. * @param {Iterable} cut_set
  1336. * @returns 差集
  1337. */
  1338. diff: function(main_set, cut_set) {
  1339. const _diff = new Set(main_set);
  1340. for (let elem of cut_set) {
  1341. _diff.delete(elem);
  1342. }
  1343. return _diff;
  1344. },
  1345.  
  1346. /**
  1347. * 增强按钮(默认为蓝色按钮:展开文档)的点击效果
  1348. * @param {string} i 按钮序号
  1349. */
  1350. enhance_click: async function(i) {
  1351. let btn = this.btn(i);
  1352. const style = btn.getAttribute("style") || "";
  1353. // 变黑缩小
  1354. btn.setAttribute(
  1355. "style",
  1356. style + "color: black; font-weight: normal;"
  1357. );
  1358. await utils.sleep(500);
  1359. btn = this.btn(i);
  1360. // 复原加粗
  1361. btn.setAttribute("style", style);
  1362. },
  1363.  
  1364. /**
  1365. * 绑定事件处理函数到指定按钮,返回实际添加的事件处理函数
  1366. * @param {(event: PointerEvent) => Promise<void>} listener click监听器
  1367. * @param {number} i 按钮序号
  1368. * @param {string} new_text 按钮的新文本,为null则不替换
  1369. * @returns {Function} 事件处理函数
  1370. */
  1371. onclick: function(listener, i, new_text=null) {
  1372. const btn = this.btn(i);
  1373.  
  1374. // 如果需要,替换按钮内文本
  1375. if (new_text) {
  1376. btn.textContent = new_text;
  1377. }
  1378.  
  1379. // 绑定事件,添加到页面上
  1380. /**
  1381. * @param {PointerEvent} event
  1382. */
  1383. async function wrapped_listener(event) {
  1384. const btn = event.target;
  1385. const text = btn.textContent;
  1386. btn.disabled = true;
  1387. try {
  1388. await listener.call(btn, event);
  1389. } catch(err) {
  1390. console.error(err);
  1391. }
  1392. btn.disabled = false;
  1393. btn.textContent = text;
  1394. }
  1395.  
  1396. btn.onclick = wrapped_listener;
  1397. return wrapped_listener;
  1398. },
  1399.  
  1400. /**
  1401. * 返回第 index 个按钮引用
  1402. * @param {number} i
  1403. * @returns {HTMLButtonElement}
  1404. */
  1405. btn: function(i) {
  1406. return wk$(`.wk-box [class="btn-${i}"]`)[0];
  1407. },
  1408.  
  1409. /**
  1410. * 强制隐藏元素
  1411. * @param {string | Array<HTMLElement>} selector_or_elems
  1412. */
  1413. force_hide: function(selector_or_elems) {
  1414. const cls = "force-hide";
  1415. const elems = selector_or_elems instanceof Array ?
  1416. selector_or_elems : wk$(selector_or_elems);
  1417.  
  1418. elems.forEach(elem => {
  1419. elem.classList.add(cls);
  1420. });
  1421.  
  1422. // 判断css样式是否已经存在
  1423. let style = wk$(`style.${cls}`)[0];
  1424. // 如果已经存在,则无须重复创建
  1425. if (style) {
  1426. return;
  1427. }
  1428. // 否则创建
  1429. style = document.createElement("style");
  1430. style.innerHTML = `style.${cls} {
  1431. visibility: hidden !important;
  1432. display: none !important;
  1433. }`;
  1434. document.head.append(style);
  1435. },
  1436.  
  1437. /**
  1438. * 等待直到元素可见。最多等待5秒。
  1439. * @param {HTMLElement} elem 一个元素
  1440. * @returns {Promise<HTMLElement>} elem
  1441. */
  1442. until_visible: async function(elem) {
  1443. let [max, i] = [25, 0];
  1444. let style = getComputedStyle(elem);
  1445. // 如果不可见就等待0.2秒/轮
  1446. while (i <= max &&
  1447. (style.display === "none" ||
  1448. style.visibility !== "hidden")
  1449. ) {
  1450. i++;
  1451. style = getComputedStyle(elem);
  1452. await this.sleep(200);
  1453. }
  1454. return elem;
  1455. },
  1456.  
  1457. /**
  1458. * 等待直到函数返回true
  1459. * @param {Function} isReady 判断条件达成与否的函数
  1460. * @param {number} timeout 最大等待秒数, 默认5000毫秒
  1461. */
  1462. wait_until: async function(isReady, timeout=5000) {
  1463. const gap = 200;
  1464. let chances = parseInt(timeout / gap);
  1465. chances = chances < 1 ? 1 : chances;
  1466. while (! await isReady()) {
  1467. await this.sleep(200);
  1468. chances -= 1;
  1469. if (!chances) {
  1470. break;
  1471. }
  1472. }
  1473. },
  1474.  
  1475. /**
  1476. * 隐藏按钮,打印页面,显示按钮
  1477. */
  1478. print_page: function() {
  1479. // 隐藏按钮,然后打印页面
  1480. this.toggle_box();
  1481. setTimeout(window.print, 500);
  1482. setTimeout(this.toggle_box, 1000);
  1483. },
  1484.  
  1485. /**
  1486. * 切换按钮显示/隐藏状态
  1487. * @param {number} i 按钮序号
  1488. * @returns 按钮元素的引用
  1489. */
  1490. toggle_btn: function(i) {
  1491. const btn = this.btn(i);
  1492. const display = getComputedStyle(btn).display;
  1493. if (display === "none") {
  1494. btn.style.display = "block";
  1495. } else {
  1496. btn.style.display = "none";
  1497. }
  1498. return btn;
  1499. },
  1500.  
  1501. /**
  1502. * 用input框跳转到对应页码
  1503. * @param {HTMLInputElement} input 当前页码
  1504. * @param {string | number} page_num 目标页码
  1505. * @param {string} type 键盘事件类型:"keyup" | "keypress" | "keydown"
  1506. */
  1507. to_page: function(input, page_num, type) {
  1508. // 设置跳转页码为目标页码
  1509. input.value = `${page_num}`;
  1510. // 模拟回车事件来跳转
  1511. const enter = new KeyboardEvent(type, {
  1512. bubbles: true,
  1513. cancelable: true,
  1514. keyCode: 13
  1515. });
  1516. input.dispatchEvent(enter);
  1517. },
  1518.  
  1519. /**
  1520. * 判断给定的url是否与当前页面同源
  1521. * @param {string} url
  1522. * @returns {boolean}
  1523. */
  1524. is_same_origin: function(url) {
  1525. url = new URL(url);
  1526. if (url.protocol === "data:") {
  1527. return true;
  1528. }
  1529. if (location.protocol === url.protocol
  1530. && location.host === url.host
  1531. && location.port === url.port
  1532. ) {
  1533. return true;
  1534. }
  1535. return false;
  1536. },
  1537.  
  1538. /**
  1539. * 在新标签页打开链接,如果提供文件名则下载
  1540. * @param {string} url
  1541. * @param {string} fname 下载文件的名称,默认为空,代表不下载
  1542. */
  1543. open_in_new_tab: function(url, fname="") {
  1544. const a = document.createElement("a");
  1545. a.href = url;
  1546. a.target = "_blank";
  1547. if (fname && this.is_same_origin(url)) {
  1548. a.download = fname;
  1549. }
  1550. a.click();
  1551. },
  1552.  
  1553. /**
  1554. * 用try移除元素
  1555. * @param {HTMLElement | string} elem_or_selector
  1556. */
  1557. remove: function(elem_or_selector) {
  1558. try {
  1559. const cls = this.classof(elem_or_selector);
  1560. if (cls === "String") {
  1561. wk$(elem_or_selector).forEach(
  1562. elem => elem.remove()
  1563. );
  1564. }
  1565. else if (cls.endsWith("Element")) {
  1566. elem_or_selector.remove();
  1567. }
  1568. } catch (e) {
  1569. console.error(e);
  1570. }
  1571. },
  1572.  
  1573. /**
  1574. * 用try移除若干元素
  1575. * @param {Iterable<HTMLElement>} elements 要移除的元素列表
  1576. */
  1577. remove_multi: function(elements) {
  1578. for (const elem of elements) {
  1579. this.remove(elem);
  1580. }
  1581. },
  1582.  
  1583. /**
  1584. * 等待全部任务落定后返回值的列表
  1585. * @param {Array<Promise>} tasks
  1586. * @returns {Promise<Array>}
  1587. */
  1588. gather: async function(tasks) {
  1589. const results = await Promise.allSettled(tasks);
  1590. const values = [];
  1591.  
  1592. for (const result of results) {
  1593. // 期约成功解决且返回值不为空的才有效
  1594. if (result.status === "fulfilled"
  1595. && !([NaN, null, undefined].includes(result.value))) {
  1596. values.push(result.value);
  1597. }
  1598. }
  1599. return values;
  1600. },
  1601.  
  1602. /**
  1603. * html元素列表转为canvas列表
  1604. * @param {Array<HTMLElement>} elements
  1605. * @returns {Promise<Array<HTMLCanvasElement>>}
  1606. */
  1607. elems_to_canvases: async function(elements) {
  1608. if (!globalThis.html2canvas) {
  1609. await this.load_web_script(
  1610. "https://cdn.staticfile.org/html2canvas/1.4.1/html2canvas.min.js"
  1611. );
  1612. }
  1613.  
  1614. // 如果是空列表, 则抛出异常
  1615. if (elements.length === 0) {
  1616. throw new Error("htmlToCanvases 未得到任何html元素");
  1617. }
  1618.  
  1619. return this.gather(
  1620. elements.map(html2canvas)
  1621. );
  1622. },
  1623.  
  1624. /**
  1625. * 将html元素转为canvas再合并到pdf中,最后下载pdf
  1626. * @param {Array<HTMLElement>} elements 元素列表
  1627. * @param {string} title 文档标题
  1628. */
  1629. elems_to_pdf: async function(elements, title="文档") {
  1630. // 如果是空元素列表,终止函数
  1631. const canvases = await this.elems_to_canvases(elements);
  1632. // 控制台检查结果
  1633. console.log("生成的canvas元素如下:");
  1634. console.log(canvases);
  1635. // 合并为PDF
  1636. this.imgs_to_pdf(canvases, title);
  1637. },
  1638.  
  1639. /**
  1640. * 使用xhr异步GET请求目标url,返回响应体blob
  1641. * @param {string} url
  1642. * @returns {Promise<Blob>} blob
  1643. */
  1644. xhr_get_blob: async function(url) {
  1645. const xhr = new XMLHttpRequest();
  1646. xhr.open("GET", url);
  1647. xhr.responseType = "blob";
  1648. return new Promise((resolve, reject) => {
  1649. xhr.onload = () => {
  1650. const code = xhr.status;
  1651. if (code >= 200 && code <= 299) {
  1652. resolve(xhr.response);
  1653. }
  1654. else {
  1655. reject(new Error(`Network Error: ${code}`));
  1656. }
  1657. };
  1658. xhr.send();
  1659. });
  1660. },
  1661.  
  1662. /**
  1663. * 加载CDN脚本
  1664. * @param {string} url
  1665. */
  1666. load_web_script: async function(url) {
  1667. try {
  1668. const resp = await fetch(url);
  1669. const code = await resp.text();
  1670. Function(code)();
  1671.  
  1672. } catch(e) {
  1673. console.error(e);
  1674. // 嵌入<script>方式
  1675. return new Promise(resolve => {
  1676. const script = document.createElement("script");
  1677. script.src = url;
  1678. script.onload = resolve;
  1679. document.body.append(script);
  1680. });
  1681. }
  1682. },
  1683.  
  1684. /**
  1685. * b64编码字符串转Uint8Array
  1686. * @param {string} sBase64 b64编码的字符串
  1687. * @param {number} nBlockSize 字节数
  1688. * @returns {Uint8Array} arr
  1689. */
  1690. b64_to_bytes: function(sBase64, nBlockSize=1) {
  1691. const
  1692. sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length,
  1693. nOutLen = nBlockSize ? Math.ceil((nInLen * 3 + 1 >>> 2) / nBlockSize) * nBlockSize : nInLen * 3 + 1 >>> 2, aBytes = new Uint8Array(nOutLen);
  1694.  
  1695. for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
  1696. nMod4 = nInIdx & 3;
  1697. nUint24 |= base.b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4;
  1698. if (nMod4 === 3 || nInLen - nInIdx === 1) {
  1699. for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
  1700. aBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255;
  1701. }
  1702. nUint24 = 0;
  1703. }
  1704. }
  1705. return aBytes;
  1706. },
  1707.  
  1708. /**
  1709. * canvas转blob
  1710. * @param {HTMLCanvasElement} canvas
  1711. * @param {string} type
  1712. * @returns {Promise<Blob>}
  1713. */
  1714. canvas_to_blob: function(canvas, type="image/png") {
  1715. return new Promise(
  1716. resolve => canvas.toBlob(resolve, type, 1)
  1717. );
  1718. },
  1719.  
  1720. /**
  1721. * 合并blobs到压缩包,然后下载
  1722. * @param {Iterable<Blob>} blobs
  1723. * @param {string} base_name 文件名通用部分,如 page-1.jpg 中的 page
  1724. * @param {string} ext 扩展名,如 jpg
  1725. * @param {string} zip_name 压缩包名称
  1726. * @param {boolean} download 是否下载,可选,默认true,如果不下载则返回压缩包对象
  1727. * @returns {"Promise<JSZip | null>"}
  1728. */
  1729. blobs_to_zip: async function(blobs, base_name, ext, zip_name, download=true) {
  1730. const zip = new window.JSZip();
  1731. // 归档
  1732. for (const [i, blob] of this.enumerate(blobs)) {
  1733. zip.file(`${base_name}-${i+1}.${ext}`, blob, { binary: true });
  1734. }
  1735.  
  1736. // 导出
  1737. if (!download) {
  1738. return zip;
  1739. }
  1740.  
  1741. const zip_blob = await zip.generateAsync({ type: "blob" });
  1742. console.log(zip_blob);
  1743. this.save(`${zip_name}.zip`, zip_blob);
  1744. return null;
  1745. },
  1746.  
  1747. /**
  1748. * 存储所有canvas图形为png到一个压缩包
  1749. * @param {Iterable<HTMLCanvasElement>} canvases canvas元素列表
  1750. * @param {string} title 文档标题
  1751. */
  1752. canvases_to_zip: async function(canvases, title) {
  1753. // canvas元素转为png图像
  1754. // 所有png合并为一个zip压缩包
  1755. const tasks = [];
  1756. for (let canvas of canvases) {
  1757. tasks.push(this.canvas_to_blob(canvas));
  1758. }
  1759. const blobs = await this.gather(tasks);
  1760. this.blobs_to_zip(blobs, "page", "png", title);
  1761. },
  1762.  
  1763.  
  1764.  
  1765. /**
  1766. * 合并图像并导出PDF
  1767. * @param {Iterable<HTMLCanvasElement | Uint8Array | HTMLImageElement>} imgs 图像元素列表
  1768. * @param {string} title 文档标题
  1769. * @param {number} width (可选)页面宽度 默认 0
  1770. * @param {number} height (可选)页面高度 默认 0
  1771. * @param {boolean} blob (可选)是否返回 blob 默认 false
  1772. */
  1773. imgs_to_pdf: async function(imgs, title, width = 0, height = 0, blob=false) {
  1774. imgs = Array.from(imgs);
  1775. if (imgs.length === 0) {
  1776. this.raise("没有任何图像用于合并为PDF");
  1777. }
  1778.  
  1779. // 先获取第一个canvas用于判断竖向还是横向,以及得到页面长宽
  1780. const first = imgs[0];
  1781.  
  1782. // 如果没有手动指定canvas的长宽,则自动检测
  1783. if (!width && !height) {
  1784. // 如果是字节数组
  1785. if (first instanceof Uint8Array) {
  1786. const cover = await createImageBitmap(
  1787. new Blob([first])
  1788. );
  1789. [width, height] = [cover.width, cover.height];
  1790.  
  1791. // 如果是画布或图像元素
  1792. } else if (
  1793. first instanceof HTMLCanvasElement ||
  1794. first instanceof HTMLImageElement
  1795. ) {
  1796. if (first.width && parseInt(first.width) && parseInt(first.height)) {
  1797. [width, height] = [first.width, first.height];
  1798. } else {
  1799. const
  1800. width_str = first.style.width.replace(/(px)|(rem)|(em)/, ""),
  1801. height_str = first.style.height.replace(/(px)|(rem)|(em)/, "");
  1802. width = parseInt(width_str);
  1803. height = parseInt(height_str);
  1804. }
  1805. } else {
  1806. // 其他未知类型
  1807. throw TypeError("不能处理的画布元素类型:" + this.classof(first));
  1808. }
  1809. }
  1810. console.log(`canvas数据:宽: ${width}px,高: ${height}px`);
  1811. // 如果文档第一页的宽比长更大,则landscape,否则portrait
  1812. const orientation = width > height ? 'l' : 'p';
  1813. // jsPDF的第三个参数为format,当自定义时,参数为数字数组。
  1814. const pdf = new jspdf.jsPDF(orientation, 'px', [height, width]);
  1815.  
  1816. const last = imgs.pop();
  1817. const self = this;
  1818. // 保存每一页文档到每一页pdf
  1819. imgs.forEach((canvas, i) => {
  1820. pdf.addImage(canvas, 'png', 0, 0, width, height);
  1821. pdf.addPage();
  1822. self?.update_popup(`PDF 已经绘制 ${i + 1} 页`);
  1823. });
  1824. // 添加尾页
  1825. pdf.addImage(last, 'png', 0, 0, width, height);
  1826. // 导出文件
  1827. if (blob) {
  1828. return pdf.output("blob");
  1829. }
  1830. pdf.save(`${title}.pdf`);
  1831. },
  1832.  
  1833. /**
  1834. * imageBitMap转canvas
  1835. * @param {ImageBitmap} bmp
  1836. * @returns {HTMLCanvasElement} canvas
  1837. */
  1838. bmp_to_canvas: function(bmp) {
  1839. const canvas = document.createElement("canvas");
  1840. canvas.height = bmp.height;
  1841. canvas.width = bmp.width;
  1842. const ctx = canvas.getContext("bitmaprenderer");
  1843. ctx.transferFromImageBitmap(bmp);
  1844. return canvas;
  1845. },
  1846.  
  1847. /**
  1848. * 导出图片链接
  1849. * @param {Iterable<string>} urls
  1850. */
  1851. save_urls: function(urls) {
  1852. const _urls = Array
  1853. .from(urls)
  1854. .map((url) => {
  1855. const _url = url.trim();
  1856. if (url.startsWith("//"))
  1857. return "https:" + _url;
  1858. return _url;
  1859. })
  1860. .filter(url => url);
  1861.  
  1862. this.save("urls.csv", _urls.join("\n"), "text/csv");
  1863. },
  1864.  
  1865. /**
  1866. * 图片blobs合并并导出为单个PDF
  1867. * @param {Array<Blob>} blobs
  1868. * @param {string} title (可选)文档名称, 不含后缀, 默认为"文档"
  1869. * @param {boolean} filter (可选)是否过滤 type 不以 "image/" 开头的 blob; 默认为 true
  1870. * @param {boolean} blob (可选)是否返回 blob,默认 false
  1871. */
  1872. img_blobs_to_pdf: async function(blobs, title="文档", filter=true, blob=false) {
  1873. // 格式转换:img blob -> bmp
  1874. let tasks = blobs;
  1875. if (filter) {
  1876. tasks = blobs.filter(
  1877. blob => blob.type.startsWith("image/")
  1878. );
  1879. }
  1880. tasks = await this.gather(
  1881. tasks.map(blob => blob.arrayBuffer())
  1882. );
  1883. tasks = tasks.map(buffer => new Uint8Array(buffer));
  1884. // 导出PDF
  1885. return this.imgs_to_pdf(tasks, title, 0, 0, blob);
  1886. },
  1887.  
  1888. /**
  1889. * 下载可以简单直接请求的图片,合并到 PDF 并导出
  1890. * @param {Iterable<string>} urls 图片链接列表
  1891. * @param {string} title 文档名称
  1892. * @param {number} min_num 如果成功获取的图片数量 < min_num, 则等待 2 秒后重试; 默认 0 不重试
  1893. * @param {boolean} clear 是否在请求完成后清理控制台输出,默认false
  1894. * @param {boolean} blobs 是否返回二进制图片列表,默认 false(即直接导出PDF)
  1895. */
  1896. img_urls_to_pdf: async function(urls, title, min_num=0, clear=false, blobs=false) {
  1897. // 强制转换为迭代器类型
  1898. urls = urls[Symbol.iterator]();
  1899. const first = urls.next().value;
  1900. // 如果不符合同源策略,在打开新标签页
  1901. if (!this.is_same_origin(first)) {
  1902. console.info("URL 不符合同源策略;转为新标签页打开目标网站");
  1903. this.open_in_new_tab((new URL(first)).origin);
  1904. return;
  1905. }
  1906.  
  1907. let tasks, img_blobs, i = 3;
  1908. // 根据请求成功数量判断是否循环
  1909. do {
  1910. i -= 1;
  1911. // 发起请求
  1912. tasks = [this.xhr_get_blob(first)]; // 初始化时加入第一个
  1913. // 然后加入剩余的
  1914. for (const [j, url] of this.enumerate(urls)) {
  1915. tasks.push(this.xhr_get_blob(url));
  1916. this.update_popup(`已经请求 ${j} 张图片`);
  1917. }
  1918. // 接收响应
  1919. img_blobs = (await this.gather(tasks)).filter(
  1920. blob => blob.type.startsWith("image/")
  1921. );
  1922.  
  1923. if (clear) {
  1924. console.clear();
  1925. }
  1926.  
  1927. if (
  1928. min_num
  1929. && img_blobs.length < min_num
  1930. && i
  1931. ) {
  1932. // 下轮行动前冷却
  1933. console.log(`打盹 2 秒`);
  1934. await utils.sleep(2000);
  1935. } else {
  1936. // 结束循环
  1937. break;
  1938. }
  1939. } while (true)
  1940.  
  1941. if (blobs) return img_blobs;
  1942. await this.img_blobs_to_pdf(img_blobs, title, false);
  1943. },
  1944.  
  1945. /**
  1946. * 返回子串个数
  1947. * @param {string} str
  1948. * @param {string} sub
  1949. */
  1950. count_sub_str: function(str, sub) {
  1951. return [...str.matchAll(sub)].length;
  1952. },
  1953.  
  1954. /**
  1955. * 返回按钮区引用
  1956. * @returns
  1957. */
  1958. sec: function() {
  1959. const sec = wk$(".wk-box .btns-sec")[0];
  1960. if (!sec) throw new Error("wk 按钮区找不到");
  1961. return sec;
  1962. },
  1963.  
  1964. _monkey: function() {
  1965. const mky = wk$(".wk-box .wk-fold-btn")[0];
  1966. if (!mky) throw new Error("wk 小猴子找不到");
  1967. return mky;
  1968. },
  1969.  
  1970. /**
  1971. * 折叠按钮区,返回是否转换了状态
  1972. */
  1973. fold_box: function() {
  1974. const sec = this.sec();
  1975. const mky = this._monkey();
  1976. const display = getComputedStyle(sec).display;
  1977. if (display !== "block") return false;
  1978. // 显示 -> 隐藏
  1979. [sec, mky].forEach(
  1980. elem => elem.classList.add("folded")
  1981. );
  1982. return true;
  1983. },
  1984.  
  1985. /**
  1986. * 展开按钮区,返回是否转换了状态
  1987. */
  1988. unfold_box: function() {
  1989. const sec = this.sec();
  1990. const mky = this._monkey();
  1991. const display = getComputedStyle(sec).display;
  1992. if (display === "block") return false;
  1993. // 隐藏 -> 显示
  1994. // 显示 -> 隐藏
  1995. [sec, mky].forEach(
  1996. elem => elem.classList.remove("folded")
  1997. );
  1998. return true;
  1999. },
  2000.  
  2001. /**
  2002. * 运行基于按钮的、显示进度条的函数
  2003. * @param {number} i 按钮序号
  2004. * @param {Function} task 需要等待的耗时函数
  2005. */
  2006. run_with_prog: async function(i, task) {
  2007. const btn = utils.btn(i);
  2008. let new_btn;
  2009.  
  2010. if (!wk$("#wk-popup")[0]) {
  2011. this.add_popup();
  2012. }
  2013.  
  2014. this.fold_box();
  2015. this.toID("wk-popup");
  2016.  
  2017. new_btn = btn.cloneNode(true);
  2018. btn.replaceWith(new_btn);
  2019. this.onclick(
  2020. () => utils.toID("wk-popup"), i, "显示进度"
  2021. );
  2022.  
  2023. try {
  2024. await task();
  2025. } catch(e) {
  2026. console.error(e);
  2027. }
  2028.  
  2029. this.toID("");
  2030. this.unfold_box();
  2031. this.remove_popup();
  2032. new_btn.replaceWith(btn);
  2033. },
  2034.  
  2035. /**
  2036. * 创建5个按钮:展开文档、导出图片、导出PDF、未设定4、未设定5;除第1个外默认均为隐藏
  2037. */
  2038. create_btns: function() {
  2039. // 添加样式
  2040. document.head.insertAdjacentHTML("beforeend", style);
  2041. // 添加按钮区
  2042. document.body.insertAdjacentHTML("beforeend", box);
  2043.  
  2044. // 绑定小猴子按钮回调
  2045. const monkey = wk$(".wk-fold-btn")[0];
  2046. // 隐藏【🙈】,展开【🐵】
  2047. monkey.onclick = () => this.fold_box() || this.unfold_box();
  2048. },
  2049.  
  2050. /**
  2051. * 添加弹窗到 body, 通过 utils.toID("wk-popup") 激发
  2052. */
  2053. add_popup: function() {
  2054. document.body.insertAdjacentHTML("beforeend", popup);
  2055. },
  2056.  
  2057. /**
  2058. * 设置弹窗正文
  2059. * @param {string} text
  2060. */
  2061. update_popup: function(text) {
  2062. const body = wk$(".wk-popup-body")[0];
  2063. if (!body) return;
  2064. body.textContent = text;
  2065. },
  2066.  
  2067. /**
  2068. * 移除弹窗
  2069. */
  2070. remove_popup: function() {
  2071. this.remove(wk$(".wk-popup-container")[0]);
  2072. },
  2073.  
  2074. /**
  2075. * 滚动页面到id位置的元素处
  2076. * @param {string} id
  2077. */
  2078. toID: function(id) {
  2079. location.hash = `#${id}`;
  2080. }
  2081. };
  2082.  
  2083.  
  2084. /**
  2085. * ---------------------------------------------------------------------
  2086. * 绑定使用 this 的函数到 utils,使其均成为绑定方法
  2087. * ---------------------------------------------------------------------
  2088. */
  2089.  
  2090. /**
  2091. * 确保特定外部脚本加载的装饰器
  2092. * @param {string} global_obj_name
  2093. * @param {string} cdn_url
  2094. * @param {Function} func
  2095. * @returns
  2096. */
  2097. function ensure_script_existed(global_obj_name, cdn_url, func) {
  2098. async function inner(...args) {
  2099. if (!window[global_obj_name]) {
  2100. // 根据需要加载依赖
  2101. await utils.load_web_script(cdn_url);
  2102. }
  2103. return func(...args);
  2104. }
  2105. // 存储参数定义
  2106. base.superAssign(inner, func);
  2107. return inner;
  2108. }
  2109.  
  2110.  
  2111. /**
  2112. * 确保引用外部依赖的函数都在调用前加载了依赖
  2113. */
  2114. for (const prop of Object.keys(utils)) {
  2115. // 跳过非函数
  2116. if (
  2117. !(typeof utils[prop] === "function")
  2118. && !`${utils[prop]}`.startsWith("class")
  2119. ) {
  2120. continue;
  2121. }
  2122.  
  2123. // 绑定this到utils
  2124. if (/ this[.[][a-z_]/.test(`${utils[prop]}`)) {
  2125. // 存储参数定义
  2126. const doc = utils.help(utils[prop], false);
  2127. // 绑死this,同时提供 __func__ 来取回原先的函数
  2128. const fn = utils[prop];
  2129. utils[prop] = utils[prop].bind(utils);
  2130. utils[prop].__func__ = fn;
  2131. // 重设参数定义
  2132. utils[prop].__doc__ = doc;
  2133. }
  2134.  
  2135. // 设定 __doc__ 为访问器属性
  2136. const doc_box = [
  2137. utils.help(utils[prop], false)
  2138. ];
  2139. Object.defineProperty(utils[prop], "__doc__", {
  2140. configurable: true,
  2141. enumerable: true,
  2142. get() { return doc_box.join("\n"); },
  2143. set(new_doc) { doc_box.push(new_doc); },
  2144. });
  2145.  
  2146. // 为有外部依赖的函数做包装
  2147. let obj, url;
  2148. const name = prop.toLowerCase();
  2149.  
  2150. if (name.includes("_to_zip")) {
  2151. obj = "JSZip";
  2152. url = "https://cdn.staticfile.org/jszip/3.7.1/jszip.min.js";
  2153.  
  2154. } else if (name.includes("_to_pdf")) {
  2155. obj = "jspdf";
  2156. url = "https://cdn.staticfile.org/jspdf/2.5.1/jspdf.umd.min.js";
  2157.  
  2158. } else {
  2159. continue;
  2160. }
  2161. utils[prop] = ensure_script_existed(obj, url, utils[prop]);
  2162. }
  2163.  
  2164.  
  2165. /**
  2166. * ---------------------------------------------------------------------
  2167. * 为 utils 部分函数绑定更详细的说明
  2168. * ---------------------------------------------------------------------
  2169. */
  2170.  
  2171. utils.b64_to_bytes.__doc__ = `
  2172. /**
  2173. * b64编码字符串转Uint8Array
  2174. * @param {string} sBase64 b64编码的字符串
  2175. * @param {number} nBlockSize 字节数
  2176. * @returns {Uint8Array} arr
  2177. */
  2178. `;
  2179.  
  2180. utils.blobs_to_zip.__doc__ = `
  2181. /**
  2182. * 合并blobs到压缩包,然后下载
  2183. * @param {Iterable<Blob>} blobs
  2184. * @param {string} base_name 文件名通用部分,如 image-1.jpg 中的 image
  2185. * @param {string} ext 扩展名,如 jpg
  2186. * @param {string} zip_name 压缩包名称
  2187. */
  2188. `;
  2189.  
  2190. utils.imgs_to_pdf.__doc__ = `
  2191. /**
  2192. * 合并图像并导出PDF
  2193. * @param {Iterable<HTMLCanvasElement | Uint8Array | HTMLImageElement>} imgs 图像元素列表
  2194. * @param {string} title 文档标题
  2195. * @param {number} width (可选)页面宽度 默认 0
  2196. * @param {number} height (可选)页面高度 默认 0
  2197. * @param {boolean} blob (可选)是否返回 blob 默认 false
  2198. */
  2199. `;
  2200.  
  2201. utils.img_urls_to_pdf.__doc__ = `
  2202. /**
  2203. * 下载可以简单直接请求的图片,合并到 PDF 并导出
  2204. * @param {Iterable<string>} urls 图片链接列表
  2205. * @param {string} title 文档名称
  2206. * @param {number} min_num 如果成功获取的图片数量 < min_num, 则等待 2 秒后重试; 默认 0 不重试
  2207. * @param {boolean} clear 是否在请求完成后清理控制台输出,默认false
  2208. */
  2209. `;
  2210.  
  2211. utils.img_blobs_to_pdf.__doc__ = `
  2212. /**
  2213. * 图片blobs合并并导出为单个PDF
  2214. * @param {Array<Blob>} blobs
  2215. * @param {string} title (可选)文档名称, 不含后缀, 默认为"文档"
  2216. * @param {boolean} filter (可选)是否过滤 type 不以 "image/" 开头的 blob; 默认为 true
  2217. * @param {boolean} blob (可选)是否返回 blob
  2218. */
  2219. `;
  2220.  
  2221.  
  2222. /**
  2223. * ---------------------------------------------------------------------
  2224. * 绑定 utils 成员到 wk$,允许外部轻松调用
  2225. * ---------------------------------------------------------------------
  2226. */
  2227.  
  2228. base.superAssign(wk$, utils);
  2229. console.info("wk: `wk$` 已经挂载到全局");
  2230.  
  2231. /**
  2232. * 展开道客巴巴的文档
  2233. */
  2234. async function readAllDoc88() {
  2235. // 获取“继续阅读”按钮
  2236. let continue_btn = wk$("#continueButton")[0];
  2237. // 如果存在“继续阅读”按钮
  2238. if (continue_btn) {
  2239. // 跳转到文末(等同于展开全文)
  2240. let cur_page = wk$("#pageNumInput")[0];
  2241. // 取得最大页码
  2242. let page_max = cur_page.parentElement.textContent.replace(" / ", "");
  2243. // 跳转到尾页
  2244. utils.to_page(cur_page, page_max, "keypress");
  2245. // 返回顶部
  2246. await utils.sleep(1000);
  2247. utils.to_page(cur_page, "1", "keypress");
  2248. }
  2249. // 文档展开后,显示按钮
  2250. else {
  2251. for (const i of utils.range(1, 6)) {
  2252. utils.toggle_btn(i);
  2253. }
  2254. }
  2255. }
  2256.  
  2257.  
  2258. /**
  2259. * 隐藏选择文字的弹窗
  2260. */
  2261. async function hideSelectPopup() {
  2262. const
  2263. elem = (await wk$$("#left-menu"))[0],
  2264. hide = elem => elem.style.zIndex = -1;
  2265. return utils.until_visible(elem).then(hide);
  2266. }
  2267.  
  2268.  
  2269. /**
  2270. * 初始化任务
  2271. */
  2272. async function initService() {
  2273. // 初始化
  2274. console.log("正在执行初始化任务");
  2275.  
  2276. // 1. 查找复制文字可能的api名称
  2277. const prop = getCopyAPIValue();
  2278. globalThis.doc88JS._apis = Object
  2279. .getOwnPropertyNames(prop)
  2280. .filter(name => {
  2281. if (!name.startsWith("_")) {
  2282. return false;
  2283. }
  2284. if (prop[name] === "") {
  2285. return true;
  2286. }
  2287. });
  2288. // 2. 隐藏选中文字的提示框
  2289. await hideSelectPopup();
  2290. // 3. 隐藏搜索框
  2291. // hideSearchBox();
  2292. // 4. 移除vip复制弹窗
  2293. // hideCopyPopup();
  2294. }
  2295.  
  2296.  
  2297. /**
  2298. * 取得 doc88JS.copy_api 所指向属性的值
  2299. * @returns
  2300. */
  2301. function getCopyAPIValue() {
  2302. let aim = globalThis;
  2303. for (let name of globalThis.doc88JS.copy_api) {
  2304. aim = aim[name];
  2305. }
  2306. return aim;
  2307. }
  2308.  
  2309.  
  2310. /**
  2311. * 返回选中的文字
  2312. * @returns {string}
  2313. */
  2314. function getSelectedText() {
  2315. // 首次复制文字,需要先找出api
  2316. if (globalThis.doc88JS.copy_api.length === 3) {
  2317. // 拼接出路径,得到属性
  2318. let prop = getCopyAPIValue(); // 此时是属性,尚未取得值
  2319.  
  2320. // 查询值
  2321. for (let name of globalThis.doc88JS._apis) {
  2322. let value = prop[name];
  2323. // 值从空字符串变为非空字符串了,确认是目标api名称
  2324. if (typeof value === 'string'
  2325. && value.length > 0
  2326. && !value.match(/\d/) // 开头不能是数字,因为可能是 '1-179-195' 这种值
  2327. ) {
  2328. globalThis.doc88JS.copy_api.push(name);
  2329. break;
  2330. }
  2331. }
  2332. }
  2333. return getCopyAPIValue();
  2334. }
  2335.  
  2336.  
  2337. /**
  2338. * 输出选中的文字到剪贴板和控制台,返回是否复制了文档
  2339. * @returns {boolean} doc_is_copied
  2340. */
  2341. function copySelected() {
  2342. // 尚未选中文字
  2343. if (getComputedStyle(wk$("#left-menu")[0]).display === "none") {
  2344. console.log("尚未选中文字");
  2345. return false;
  2346. }
  2347. // 输出到控制台和剪贴板
  2348. utils.copy_text(getSelectedText());
  2349. return true;
  2350. }
  2351.  
  2352.  
  2353. /**
  2354. * 捕获 ctrl + c 以复制文字
  2355. * @param {KeyboardEvent} e
  2356. * @returns
  2357. */
  2358. function onCtrlC(e) {
  2359. // 判断是否为 ctrl + c
  2360. if (!(e.code === "KeyC" && e.ctrlKey === true)) {
  2361. return;
  2362. }
  2363.  
  2364. // 判断触发间隔
  2365. let now = Date.now();
  2366. // 距离上次小于0.5秒
  2367. if (now - doc88JS.last_copy_time < 500 * 1) {
  2368. doc88JS.last_copy_time = now;
  2369. return;
  2370. }
  2371. // 大于1秒
  2372. // 刷新最近一次触发时间
  2373. doc88JS.last_copy_time = now;
  2374. // 复制文字
  2375. copySelected();
  2376. // if (!copySelected()) return;
  2377. // 停止传播
  2378. e.stopImmediatePropagation();
  2379. e.stopPropagation();
  2380. }
  2381.  
  2382.  
  2383. /**
  2384. * 浏览并加载所有页面
  2385. */
  2386. async function walkThrough$2() {
  2387. // 文档容器
  2388. let container = wk$("#pageContainer")[0];
  2389. container.style.display = "none";
  2390. // 页码
  2391. let page_num = wk$("#pageNumInput")[0];
  2392. // 文末提示
  2393. let tail = wk$("#readEndDiv > p")[0];
  2394. let origin = tail.textContent;
  2395. // 按钮
  2396. wk$('.btns_section > [class*="btn-"]').forEach(
  2397. elem => elem.style.display = "none"
  2398. );
  2399.  
  2400. // 逐页渲染
  2401. let total = parseInt(Config.p_pagecount);
  2402. try {
  2403. for (let i = 1; i <= total; i++) {
  2404. // 前往页码
  2405. GotoPage(i);
  2406. await utils.wait_until(async() => {
  2407. let page = wk$(`#page_${i}`)[0];
  2408. // page无法选中说明有弹窗
  2409. if (!page) {
  2410. // 关闭弹窗,等待,然后递归
  2411. wk$("#ym-window .DOC88Window_close")[0].click();
  2412. await utils.sleep(500);
  2413. walkThrough$2();
  2414. throw new Error("walkThrough 递归完成,终止函数");
  2415. }
  2416. // canvas尚未绘制时width=300
  2417. return page.width !== 300;
  2418. });
  2419. // 凸显页码
  2420. utils.emphasize_text(page_num);
  2421. tail.textContent = `请勿反复点击按钮,耐心等待页面渲染:${i}/${total}`;
  2422. }
  2423. } catch(e) {
  2424. // 捕获退出信号,然后退出
  2425. console.log(e);
  2426. return;
  2427. }
  2428.  
  2429. // 恢复原本显示
  2430. container.style.display = "";
  2431. page_num.style = "";
  2432. tail.textContent = origin;
  2433. // 按钮
  2434. wk$('.btns_section > [class*="btn-"]').forEach(
  2435. elem => elem.style.display = "block"
  2436. );
  2437. wk$(".btns_section > .btn-1")[0].style.display = "none";
  2438. }
  2439.  
  2440.  
  2441. /**
  2442. * 道客巴巴文档下载策略
  2443. */
  2444. async function doc88() {
  2445. // 全局对象
  2446. globalThis.doc88JS = {
  2447. last_copy_time: 0, // 上一次 ctrl + c 的时间戳(毫秒)
  2448. copy_api: ["Core", "Annotation", "api"]
  2449. };
  2450.  
  2451. // 创建脚本启动按钮1、2
  2452. utils.create_btns();
  2453.  
  2454. // 绑定主函数
  2455. let prepare = function() {
  2456. // 获取canvas元素列表
  2457. let node_list = wk$(".inner_page");
  2458. // 获取文档标题
  2459. let title;
  2460. if (wk$(".doctopic h1")[0]) {
  2461. title = wk$(".doctopic h1")[0].title;
  2462. } else {
  2463. title = "文档";
  2464. }
  2465. return [node_list, title];
  2466. };
  2467.  
  2468. // btn_1: 展开文档
  2469. utils.onclick(readAllDoc88, 1);
  2470.  
  2471. // // btn_2: 加载全部页面
  2472. utils.onclick(walkThrough$2, 2, "加载所有页面");
  2473. // btn_3: 导出PDF
  2474. function imgsToPDF() {
  2475. if (confirm("确定每页内容都加载完成了吗?")) {
  2476. utils.run_with_prog(
  2477. 3, () => utils.imgs_to_pdf(...prepare())
  2478. );
  2479. }
  2480. } utils.onclick(imgsToPDF, 3, "导出图片到PDF");
  2481.  
  2482. // btn_4: 导出ZIP
  2483. utils.onclick(() => {
  2484. if (confirm("确定每页内容都加载完成了吗?")) {
  2485. utils.canvases_to_zip(...prepare());
  2486. }
  2487. }, 4, "导出图片到ZIP");
  2488.  
  2489. // btn_5: 复制选中文字
  2490. utils.onclick(btn => {
  2491. if (!copySelected()) {
  2492. btn.textContent = "未选中文字";
  2493. } else {
  2494. btn.textContent = "复制成功!";
  2495. }
  2496. }, 5, "复制选中文字");
  2497.  
  2498. // 为 ctrl + c 添加响应
  2499. window.addEventListener("keydown", onCtrlC, true);
  2500. // 执行一次初始化任务
  2501. window.addEventListener(
  2502. "mousedown", initService, { once: true, capture: true }
  2503. );
  2504. }
  2505.  
  2506. function get_title$1() {
  2507. return document.title.slice(0,-6);
  2508. }
  2509.  
  2510.  
  2511. function save_canvases(type) {
  2512. return () => {
  2513. if (!wk$(".hkswf-content2 canvas").length) {
  2514. alert("当前页面不适用此按钮");
  2515. return;
  2516. }
  2517. if (confirm("页面加载完毕了吗?")) {
  2518. const title = get_title$1();
  2519. const canvases = wk$(".hkswf-content2 canvas");
  2520. let data_to;
  2521.  
  2522. switch (type) {
  2523. case "pdf":
  2524. data_to = utils.imgs_to_pdf;
  2525. break;
  2526.  
  2527. case "zip":
  2528. data_to = utils.canvases_to_zip;
  2529. break;
  2530. default:
  2531. data_to = () => utils.raise(`未知 type: ${type}`);
  2532. break;
  2533. }
  2534. data_to(canvases, title);
  2535. }
  2536. }
  2537. }
  2538.  
  2539.  
  2540. function get_base_url() {
  2541. // https://docimg1.docin.com/docinpic.jsp?file=2179420769&width=1000&sid=bZh4STs-f4NA88IA02INyapgA9Z5X3NN1sGo4WnpquIvk4CyflMk1Oxey1BsO1BG&pageno=2&pcimg=1
  2542. return `https://docimg1.docin.com/docinpic.jsp?` +
  2543. `file=` + location.pathname.match(/p-(\d+)[.]html/)[1] +
  2544. `&width=1000&sid=` + window.readerConfig.flash_param_hzq +
  2545. `&pcimg=1&pageno=`;
  2546. }
  2547.  
  2548.  
  2549. /**
  2550. * 返回总页码
  2551. * @returns {number}
  2552. */
  2553. function get_page_num() {
  2554. return parseInt(
  2555. wk$(".page_num")[0].textContent.slice(1)
  2556. );
  2557. }
  2558.  
  2559. function init_save_imgs() {
  2560. const iframe = document.createElement("iframe");
  2561. iframe.src = "https://docimg1.docin.com/?wk=true";
  2562. iframe.style.display = "none";
  2563. let sock;
  2564.  
  2565. /**
  2566. * @param {MessageEvent} event
  2567. */
  2568. function on_client_msg(event) {
  2569. if (event.data.author !== "wk"
  2570. || event.data.action !== "finish"
  2571. ) return;
  2572. sock.notListen(on_client_msg);
  2573. iframe.remove();
  2574. utils.toggle_btn(1);
  2575. utils.toggle_btn(3);
  2576. }
  2577. /**
  2578. * @param {string} type "pdf" | "zip"
  2579. */
  2580. return (type) => {
  2581. return async function() {
  2582. if (!wk$("[id*=img_] img").length) {
  2583. alert("当前页面不适用此按钮");
  2584. return;
  2585. }
  2586. utils.toggle_btn(1);
  2587. utils.toggle_btn(3);
  2588.  
  2589. document.body.append(iframe);
  2590. await utils.sleep(500);
  2591. sock = new utils.Socket(iframe.contentWindow);
  2592. await sock.connect(false);
  2593. sock.listen(on_client_msg);
  2594. sock.talk({
  2595. author: "wk",
  2596. type,
  2597. title: get_title$1(),
  2598. base_url: get_base_url(),
  2599. max: get_page_num()
  2600. });
  2601. }
  2602. }
  2603. }
  2604.  
  2605.  
  2606. const save_imgs = init_save_imgs();
  2607.  
  2608.  
  2609. async function walk_through() {
  2610. // 隐藏按钮
  2611. utils.toggle_btn(5);
  2612. // 隐藏文档页面
  2613. wk$("#contentcontainer")[0].setAttribute("style", "visibility: hidden;");
  2614.  
  2615. const total = get_page_num();
  2616. const input = wk$("#page_cur")[0];
  2617. for (let i = 1; i <= total; i++) {
  2618. utils.to_page(input, i, "keydown");
  2619. await utils.wait_until(
  2620. () => {
  2621. const page = wk$(`#page_${i}`)[0];
  2622. const contents = wk$.call(page, `.canvas_loaded, img`);
  2623. return contents.length > 0;
  2624. },
  2625. 5000
  2626. );
  2627. }
  2628.  
  2629. // 显示文档页面
  2630. wk$("#contentcontainer")[0].removeAttribute("style");
  2631. }
  2632.  
  2633.  
  2634. function main_page() {
  2635. // 创建脚本启动按钮
  2636. utils.create_btns();
  2637. utils.onclick(
  2638. save_imgs("pdf"), 1, "合并图片为PDF"
  2639. );
  2640. utils.onclick(
  2641. save_canvases("pdf"), 2, "合并画布为PDF"
  2642. );
  2643. utils.toggle_btn(2);
  2644.  
  2645. utils.onclick(
  2646. save_imgs("zip"), 3, "打包图片到ZIP"
  2647. );
  2648. utils.toggle_btn(3);
  2649. utils.onclick(
  2650. save_canvases("zip"), 4, "打包画布到ZIP"
  2651. );
  2652. utils.toggle_btn(4);
  2653.  
  2654. utils.onclick(
  2655. walk_through, 5, "自动浏览页面"
  2656. );
  2657. utils.toggle_btn(5);
  2658. }
  2659.  
  2660.  
  2661.  
  2662. function init_background() {
  2663. const sock = new utils.Socket(window.top);
  2664.  
  2665. /**
  2666. * @param {MessageEvent} event
  2667. */
  2668. async function on_server_msg(event) {
  2669. if (event.data.author !== "wk") return;
  2670. const { title, base_url, max, type } = event.data;
  2671. const urls = Array
  2672. .from(utils.range(1, max + 1))
  2673. .map(i => (base_url + i));
  2674. const imgs = await utils.img_urls_to_pdf(
  2675. urls, title, 0, false, true
  2676. );
  2677. switch (type) {
  2678. case "pdf":
  2679. await utils.img_blobs_to_pdf(imgs, title);
  2680. break;
  2681. case "zip":
  2682. const ext = imgs[0].type ? imgs[0].type.split("/")[1] : "png";
  2683. await utils.blobs_to_zip(
  2684. imgs, "page", ext, title
  2685. );
  2686. break;
  2687.  
  2688. default:
  2689. utils.raise(`未知 type: ${type}`);
  2690. break;
  2691. }
  2692.  
  2693. sock.talk({
  2694. author: "wk",
  2695. action: "finish"
  2696. });
  2697. sock.notListen(on_server_msg);
  2698. }
  2699. return async function() {
  2700. sock.listen(on_server_msg);
  2701. await sock.connect(true);
  2702. }
  2703. }
  2704.  
  2705.  
  2706. const background = init_background();
  2707.  
  2708.  
  2709. /**
  2710. * 豆丁文档下载策略
  2711. */
  2712. function docin() {
  2713. const host = location.hostname;
  2714. switch (host) {
  2715. case "jz.docin.com":
  2716. case "www.docin.com":
  2717. main_page();
  2718. break;
  2719.  
  2720. case "docimg1.docin.com":
  2721. background();
  2722. break;
  2723. default:
  2724. console.log(`未知域名: ${host}`);
  2725. break;
  2726. }
  2727. }
  2728.  
  2729. function jumpToHost() {
  2730. // https://swf.ishare.down.sina.com.cn/1DrH4Qt2cvKd.jpg?ssig=DUf5x%2BXnKU&Expires=1673867307&KID=sina,ishare&range={}-{}
  2731. let url = wk$(".data-detail img, .data-detail embed")[0].src;
  2732. if (!url) {
  2733. alert("找不到图片元素");
  2734. return;
  2735. }
  2736.  
  2737. let url_obj = new URL(url);
  2738. let path = url_obj.pathname.slice(1);
  2739. let query = url_obj.search.slice(1).split("&range")[0];
  2740. let title = document.title.split(" - ")[0];
  2741. let target = `${url_obj.protocol}//${url_obj.host}?path=${path}&fname=${title}&${query}`;
  2742. // https://swf.ishare.down.sina.com.cn/
  2743. globalThis.open(target, "hostage");
  2744. // 然后在跳板页面发起对图片的请求
  2745. }
  2746.  
  2747.  
  2748. /**
  2749. * 爱问文库下载跳转策略
  2750. */
  2751. function ishare() {
  2752. // 创建按钮区
  2753. utils.create_btns();
  2754.  
  2755. // btn_1: 识别文档类型 -> 导出PDF
  2756. utils.onclick(jumpToHost, 1, "到下载页面");
  2757. // btn_2: 不支持爱问办公
  2758. utils.onclick(() => null, 2, "不支持爱问办公");
  2759. // utils.toggleBtnStatus(4);
  2760. }
  2761.  
  2762. /**
  2763. * 返回包含对于数量svg元素的html元素
  2764. * @param {string} data
  2765. * @returns {HTMLDivElement} article
  2766. */
  2767. function _createDiv(data) {
  2768. let num = utils.count_sub_str(data, data.slice(0, 10));
  2769. let article = document.createElement("div");
  2770. article.id = "article";
  2771. article.innerHTML = `
  2772. <style class="wk-settings">
  2773. body {
  2774. margin: 0px;
  2775. width: 100%;
  2776. background-color: rgb(95,99,104);
  2777. }
  2778. #article {
  2779. width: 100%;
  2780. display: flex;
  2781. flex-direction: row;
  2782. justify-content: space-around;
  2783. }
  2784. #root-box {
  2785. display: flex;
  2786. flex-direction: column;
  2787. background-color: white;
  2788. padding: 0 2em;
  2789. }
  2790. .gap {
  2791. height: 50px;
  2792. width: 100%;
  2793. background-color: transparent;
  2794. }
  2795. </style>
  2796. <div id="root-box">
  2797. ${
  2798. `<object class="svg-box"></object>
  2799. <div class="gap"></div>`.repeat(num)
  2800. }
  2801. `;
  2802. // 移除最后一个多出的gap
  2803. Array.from(article.querySelectorAll(".gap")).at(-1).remove();
  2804. return article;
  2805. }
  2806.  
  2807.  
  2808. function setGap(height) {
  2809. let style = wk$(".wk-settings")[0].innerHTML;
  2810. wk$(".wk-settings")[0].innerHTML = style.replace(
  2811. /[.]gap.*?{.*?height:.+?;/s,
  2812. `.gap { height: ${parseInt(height)}px;`
  2813. );
  2814. }
  2815.  
  2816.  
  2817. function setGapGUI() {
  2818. let now = getComputedStyle(wk$(".gap")[0]).height;
  2819. let new_h = prompt(`当前间距:${now}\n请输入新间距:`);
  2820. if (new_h) {
  2821. setGap(new_h);
  2822. }
  2823. }
  2824.  
  2825.  
  2826. function getSVGtext(data) {
  2827. let div = document.createElement("div");
  2828. div.innerHTML = data;
  2829. return div.textContent;
  2830. }
  2831.  
  2832.  
  2833. function toDisplayMode1() {
  2834. let content = globalThis["ishareJS"].content_1;
  2835. if (!content) {
  2836. content = globalThis["ishareJS"].text
  2837. .replace(/\n{2,}/g, "<hr>")
  2838. .replace(/\n/g, "<br>")
  2839. .replace(/\s/g, "&nbsp;")
  2840. .replace(/([a-z])([A-Z])/g, "$1 $2"); // 英文简单分词
  2841.  
  2842. globalThis["ishareJS"].content_1 = content;
  2843. }
  2844.  
  2845. wk$("#root-box")[0].innerHTML = content;
  2846. }
  2847.  
  2848.  
  2849. function toDisplayMode2() {
  2850. let content = globalThis["ishareJS"].content_2;
  2851. if (!content) {
  2852. content = globalThis["ishareJS"].text
  2853. .replace(/\n{2,}/g, "<hr>")
  2854. .replace(/\n/g, "")
  2855. .replace(/\s/g, "&nbsp;")
  2856. .replace(/([a-z])([A-Z])/g, "$1 $2")
  2857. .split("<hr>")
  2858. .map(paragraph => `<p>${paragraph}</p>`)
  2859. .join("");
  2860. globalThis["ishareJS"].content_2 = content;
  2861. wk$(".wk-settings")[0].innerHTML += `
  2862. #root-box > p {
  2863. text-indent: 2em;
  2864. width: 40em;
  2865. word-break: break-word;
  2866. }
  2867. `;
  2868. }
  2869.  
  2870. wk$("#root-box")[0].innerHTML = content;
  2871. }
  2872.  
  2873.  
  2874. function changeDisplayModeWrapper() {
  2875. let flag = true;
  2876.  
  2877. function inner() {
  2878. if (flag) {
  2879. toDisplayMode1();
  2880. } else {
  2881. toDisplayMode2();
  2882. }
  2883. flag = !flag;
  2884. }
  2885. return inner;
  2886. }
  2887.  
  2888.  
  2889. function handleSVGtext() {
  2890. globalThis["ishareJS"].text = getSVGtext(
  2891. globalThis["ishareJS"].data
  2892. );
  2893.  
  2894. let change = changeDisplayModeWrapper();
  2895. utils.onclick(change, 4, "切换显示模式");
  2896.  
  2897. utils.toggle_btn(2);
  2898. utils.toggle_btn(3);
  2899. utils.toggle_btn(4);
  2900. change();
  2901. }
  2902.  
  2903.  
  2904. /**
  2905. * 处理svg的url
  2906. * @param {string} svg_url
  2907. */
  2908. async function handleSVGurl(svg_url) {
  2909. let resp = await fetch(svg_url);
  2910. let data = await resp.text();
  2911. globalThis["ishareJS"].data = data;
  2912.  
  2913. let sep = data.slice(0, 10);
  2914. let svg_texts = data
  2915. .split(sep)
  2916. .slice(1)
  2917. .map(svg_text => sep + svg_text);
  2918.  
  2919. console.log(`共 ${svg_texts.length} 张图片`);
  2920.  
  2921. let article = _createDiv(data);
  2922. let boxes = article.querySelectorAll(".svg-box");
  2923. boxes.forEach((obj, i) => {
  2924. let blob = new Blob([svg_texts[i]], {type: "image/svg+xml"});
  2925. let url = URL.createObjectURL(blob);
  2926. obj.data = url;
  2927. URL.revokeObjectURL(blob);
  2928. });
  2929.  
  2930. let body = wk$("body")[0];
  2931. body.innerHTML = "";
  2932. body.appendChild(article);
  2933.  
  2934. utils.create_btns();
  2935. utils.onclick(utils.print_page, 1, "打印页面到PDF");
  2936. utils.onclick(setGapGUI, 2, "重设页间距");
  2937. utils.onclick(handleSVGtext, 3, "显示空白点我");
  2938.  
  2939. utils.toggle_btn(2);
  2940. utils.toggle_btn(3);
  2941. }
  2942.  
  2943.  
  2944. /**
  2945. * 取得图片下载地址
  2946. * @param {string} fname
  2947. * @param {string} path
  2948. * @returns
  2949. */
  2950. function getImgUrl(fname, path) {
  2951. if (!fname) {
  2952. throw new Error("URL Param `fname` does not exist.");
  2953. }
  2954. return location.href
  2955. .replace(/[?].+?&ssig/, "?ssig")
  2956. .replace("?", path + "?");
  2957. }
  2958.  
  2959.  
  2960. /**
  2961. * 下载整个图片包
  2962. * @param {string} img_url
  2963. * @returns
  2964. */
  2965. async function getData(img_url) {
  2966. let resp = await fetch(img_url);
  2967. // window.data = await resp.blob();
  2968. // throw Error("stop");
  2969. let buffer = await resp.arrayBuffer();
  2970. return new Uint8Array(buffer);
  2971. }
  2972.  
  2973.  
  2974. /**
  2975. * 分切图片包为若干图片
  2976. * @param {Uint8Array} data 多张图片合集数据包
  2977. * @returns {Array<Uint8Array>} 图片列表
  2978. */
  2979. function parseData(data) {
  2980. // 判断图像类型/拿到文件头
  2981. let head = data.slice(0, 8);
  2982. return utils.split_files_by_head(data, head);
  2983. }
  2984.  
  2985.  
  2986. /**
  2987. * 图像Uint8数组列表合并然后导出PDF
  2988. * @param {string} fname
  2989. * @param {Array<Uint8Array>} img_data_list
  2990. */
  2991. async function imgDataArrsToPDF(fname, img_data_list) {
  2992. return utils.imgs_to_pdf(
  2993. img_data_list,
  2994. fname
  2995. );
  2996. }
  2997.  
  2998.  
  2999. /**
  3000. *
  3001. * @param {string} fname 文件名
  3002. * @param {Array<Uint8Array>} img_data_list 数据列表
  3003. */
  3004. async function saveAsZip(fname, img_data_list) {
  3005. await utils.blobs_to_zip(
  3006. img_data_list,
  3007. "page",
  3008. "png",
  3009. fname
  3010. );
  3011. }
  3012.  
  3013.  
  3014. /**
  3015. * 取得图片集合体并切分,如果是 SVG 则对应处理
  3016. * @returns {Array<Uint8Array>} imgs
  3017. */
  3018. async function getImgs() {
  3019. let [fname, path] = [
  3020. window.ishareJS.fname,
  3021. window.ishareJS.path
  3022. ];
  3023.  
  3024. let img_url = getImgUrl(fname, path);
  3025.  
  3026. // 处理svg
  3027. if (path.includes(".svg")) {
  3028. document.title = fname;
  3029. await handleSVGurl(img_url);
  3030. return;
  3031. }
  3032. // 处理常规图像
  3033. let data = await getData(img_url);
  3034. let img_data_list = parseData(data);
  3035. console.log(`共 ${img_data_list.length} 张图片`);
  3036.  
  3037. window.ishareJS.imgs = img_data_list;
  3038.  
  3039. // 下载完成,可以导出
  3040. utils.onclick(exportPDF$3, 2, "下载并导出PDF");
  3041. utils.toggle_btn(1);
  3042. utils.toggle_btn(2);
  3043. }
  3044.  
  3045.  
  3046. async function exportPDF$3() {
  3047. let args = [
  3048. window.ishareJS.fname,
  3049. window.ishareJS.imgs
  3050. ];
  3051.  
  3052. try {
  3053. await imgDataArrsToPDF(...args);
  3054. } catch(e) {
  3055. console.error(e);
  3056. // 因 jsPDF 字符串拼接溢出导致的 Error
  3057. if (`${e}`.includes("RangeError: Invalid string length")) {
  3058. // 提示失败
  3059. alert("图片合并为 PDF 时失败,请尝试下载图片压缩包");
  3060. // 备选方案:导出图片压缩包
  3061. utils.onclick(
  3062. () => saveAsZip(...args),
  3063. 3,
  3064. "导出ZIP"
  3065. );
  3066. utils.toggle_btn(3); // 显示导出ZIP按钮
  3067. utils.toggle_btn(2); // 隐藏导出PDF按钮
  3068. } else {
  3069. throw e;
  3070. }
  3071. }
  3072.  
  3073. }
  3074.  
  3075.  
  3076. function showHints() {
  3077. wk$("h1")[0].textContent = "wk 温馨提示";
  3078. wk$("p")[0].innerHTML = [
  3079. "下载 270 页的 PPT (70 MB) 需要约 30 秒",
  3080. "请耐心等待,无需反复点击按钮",
  3081. "如果很久没反应,请加 QQ 群反馈问题"
  3082. ].join("<br>");
  3083. wk$("hr")[0].nextSibling.textContent = "403 Page Hostaged By Wenku Doc Downloader";
  3084. }
  3085.  
  3086.  
  3087. /**
  3088. * 爱问文库下载策略
  3089. */
  3090. async function ishareData() {
  3091. // 全局对象
  3092. globalThis["ishareJS"] = {
  3093. data: "",
  3094. imgs: [],
  3095. text: "",
  3096. content_1: "",
  3097. content_2: "",
  3098. fname: utils.get_param("fname"),
  3099. path: utils.get_param("path")
  3100. };
  3101.  
  3102. // 显示提示
  3103. showHints();
  3104.  
  3105. // 创建按钮区
  3106. utils.create_btns();
  3107.  
  3108. // btn_1: 识别文档类型,处理SVG或下载数据
  3109. utils.onclick(getImgs, 1, "下载数据");
  3110. }
  3111.  
  3112. /**
  3113. * 提供提示信息
  3114. */
  3115. function showTips$1() {
  3116. const h2 = document.createElement("h2");
  3117. h2.id = "wk-tips";
  3118. document.body.append(h2);
  3119. }
  3120.  
  3121.  
  3122. /**
  3123. * 更新文字到 h2 元素
  3124. * @param {string} text
  3125. */
  3126. function update(text) {
  3127. wk$("#wk-tips")[0].textContent = text;
  3128. }
  3129.  
  3130.  
  3131. /**
  3132. * 被动连接,取出数据,请求并分割图片,导出PDF
  3133. */
  3134. function mainTask() {
  3135. const sock = new utils.Socket(opener);
  3136. sock.listen(async e => {
  3137. if (e.data.wk && e.data.action) {
  3138. update("图片下载中,请耐心等待...");
  3139.  
  3140. const url = e.data.img_url;
  3141. const resp = await fetch(url);
  3142. update("图片下载完成,正在解析...");
  3143.  
  3144. const buffer = await resp.arrayBuffer();
  3145. const whole_data = new Uint8Array(buffer);
  3146. update("图片解析完成,正在合并...");
  3147. await utils.imgs_to_pdf(
  3148. utils.split_files_by_head(whole_data),
  3149. e.data.title
  3150. );
  3151. update("图片合并完成,正在导出 PDF...");
  3152. }
  3153. });
  3154. sock.connect(true);
  3155. }
  3156.  
  3157.  
  3158. /**
  3159. * 爱问文库图片下载策略v2
  3160. * @returns
  3161. */
  3162. function ishareData2() {
  3163. showTips$1();
  3164. if (!(window.opener && window.opener.window)) {
  3165. update("wk: 抱歉,页面出错了");
  3166. return;
  3167. }
  3168. mainTask();
  3169. }
  3170.  
  3171. function getPageNum() {
  3172. // ' / 6 ' -> ' 6 '
  3173. return parseInt(
  3174. wk$("span.counts")[0].textContent.split("/")[1]
  3175. );
  3176. }
  3177.  
  3178.  
  3179. function jumpToHostage() {
  3180. const
  3181. // '/fileroot/2019-9/23/73598bfa-6b91-4cbe-a548-9996f46653a2/73598bfa-6b91-4cbe-a548-9996f46653a21.gif'
  3182. url = new URL(wk$("#pageflash_1 > img")[0].src),
  3183. num = getPageNum(),
  3184. // '七年级上册地理期末试卷精编.doc-得力文库'
  3185. fname = document.title.slice(0, -5),
  3186. path = url.pathname,
  3187. tail = "1.gif";
  3188. if (!path.endsWith(tail)) {
  3189. throw new Error(`url尾部不为【${tail}】!path:【${path}】`);
  3190. }
  3191. const base_path = path.slice(0, -5);
  3192. open(`${url.protocol}//${url.host}/?num=${num}&lmt=${lmt}&fname=${fname}&path=${base_path}`);
  3193. }
  3194.  
  3195.  
  3196. function deliwenku() {
  3197. utils.create_btns();
  3198. utils.onclick(jumpToHostage, 1, "到下载页面");
  3199. }
  3200.  
  3201. function showTips() {
  3202. const body = `
  3203. <style>
  3204. h1 { color: black; }
  3205. #main {
  3206. margin: 1vw 5%;
  3207. border-radius: 10%;
  3208. }
  3209. p { font-size: large; }
  3210. .info {
  3211. color: rgb(230,214,110);
  3212. background: rgb(39,40,34);
  3213. text-align: right;
  3214. font-size: medium;
  3215. padding: 1vw;
  3216. border-radius: 4px;
  3217. }
  3218. </style>
  3219. <div id="main">
  3220. <h1>wk: 跳板页面</h1>
  3221. <p>有时候点一次下载等半天没反应,就再试一次</p>
  3222. <p>如果试了 2 次还不行加 QQ 群反馈吧...</p>
  3223. <p>导出的 PDF 如果页面数量少于应有的,那么意味着免费页数就这么多,我也爱莫能助</p>
  3224. <p>短时间连续使用导出按钮会导致 IP 被封禁</p>
  3225. <hr>
  3226. <div class="info">
  3227. 文档名称:${deliJS.fname}<br>
  3228. 原始文档页数:${deliJS.num}<br>
  3229. 最大免费页数:${deliJS.lmt}<br>
  3230. </div>
  3231. </div>`;
  3232. document.title = utils.get_param("fname"); document.body.innerHTML = body;
  3233. }
  3234.  
  3235.  
  3236. /**
  3237. * url生成器
  3238. * @param {string} base_url
  3239. * @param {number} num
  3240. */
  3241. function* genURLs(base_url, num) {
  3242. for (let i=1; i<=num; i++) {
  3243. yield `${base_url}${i}.gif`;
  3244. }
  3245. }
  3246.  
  3247.  
  3248. function genBaseURL(path) {
  3249. return `${location.protocol}//${location.host}${path}`;
  3250. }
  3251.  
  3252.  
  3253. function parseParamsToDeliJS() {
  3254. const
  3255. base_url = genBaseURL(utils.get_param("path")),
  3256. fname = utils.get_param("fname"),
  3257. num = parseInt(utils.get_param("num"));
  3258.  
  3259. let lmt = parseInt(utils.get_param("lmt"));
  3260. lmt = lmt > 3 ? lmt : 20;
  3261. lmt = lmt > num ? num : lmt;
  3262.  
  3263. window.deliJS = {
  3264. base_url,
  3265. num,
  3266. fname,
  3267. lmt
  3268. };
  3269. }
  3270.  
  3271.  
  3272. async function exportPDF$2() {
  3273. utils.toggle_btn(1);
  3274. await utils.run_with_prog(
  3275. 1, () => utils.img_urls_to_pdf(
  3276. genURLs(deliJS.base_url, deliJS.num),
  3277. deliJS.fname,
  3278. deliJS.lmt,
  3279. true // 请求完成后清理控制台
  3280. )
  3281. );
  3282. utils.toggle_btn(1);
  3283. }
  3284.  
  3285.  
  3286. /**
  3287. * 得力文库跳板页面下载策略
  3288. */
  3289. async function deliFile() {
  3290. // 从URL解析文档参数
  3291. parseParamsToDeliJS();
  3292. // 显示提示
  3293. showTips();
  3294.  
  3295. // 创建按钮区
  3296. utils.create_btns();
  3297. // btn_1: 导出PDF
  3298. utils.onclick(exportPDF$2, 1, "导出PDF");
  3299. }
  3300.  
  3301. function readAll360Doc() {
  3302. // 展开文档
  3303. document.querySelector(".article_showall a").click();
  3304. // 隐藏按钮
  3305. utils.toggle_btn(1);
  3306. // 显示按钮
  3307. utils.toggle_btn(2);
  3308. utils.toggle_btn(3);
  3309. utils.toggle_btn(4);
  3310. }
  3311.  
  3312.  
  3313. function saveText_360Doc() {
  3314. // 捕获图片链接
  3315. let images = wk$("#artContent img");
  3316. let content = [];
  3317.  
  3318. for (let i = 0; i < images.length; i++) {
  3319. let src = images[i].src;
  3320. content.push(`图${i+1},链接:${src}`);
  3321. }
  3322. // 捕获文本
  3323. let text = wk$("#artContent")[0].textContent;
  3324. content.push(text);
  3325.  
  3326. // 保存纯文本文档
  3327. let title = wk$("#titiletext")[0].textContent;
  3328. utils.save(`${title}.txt`, content.join("\n"));
  3329. }
  3330.  
  3331.  
  3332. /**
  3333. * 使文档在页面上居中
  3334. * @param {string} selector 文档容器的css选择器
  3335. * @param {string} default_offset 文档部分向右偏移的百分比(0-59)
  3336. * @returns 偏移值是否合法
  3337. */
  3338. function centre(selector, default_offset) {
  3339. const elem = wk$(selector)[0];
  3340. const offset = prompt("请输入偏移百分位:", default_offset);
  3341. // 如果输入的数字不在 0-59 内,提醒用户重新设置
  3342. if (offset.length === 1 && offset.search(/[0-9]/) !== -1) {
  3343. elem.style.marginLeft = offset + "%";
  3344. return true;
  3345. }
  3346.  
  3347. if (offset.length === 2 && offset.search(/[1-5][0-9]/) !== -1) {
  3348. elem.style.marginLeft = offset + "%";
  3349. return true;
  3350. }
  3351.  
  3352. alert("请输入一个正整数,范围在0至59之间,用来使文档居中");
  3353. return false;
  3354. }
  3355.  
  3356.  
  3357. function printPage360Doc() {
  3358. if (!confirm("确定每页内容都加载完成了吗?")) {
  3359. return;
  3360. }
  3361. // # 清理并打印360doc的文档页
  3362. // ## 移除页面上无关的元素
  3363. let selector = ".fontsize_bgcolor_controler, .atfixednav, .header, .a_right, .article_data, .prev_next, .str_border, .youlike, .new_plbox, .str_border, .ul-similar, #goTop2, #divtort, #divresaveunder, .bottom_controler, .floatqrcode";
  3364. let elem_list = wk$(selector);
  3365. let under_doc_1, under_doc_2;
  3366. try {
  3367. under_doc_1 = wk$("#bgchange p.clearboth")[0].nextElementSibling;
  3368. under_doc_2 = wk$("#bgchange")[0].nextElementSibling.nextElementSibling;
  3369. } catch (e) {}
  3370. // 执行移除
  3371. for (let elem of elem_list) {
  3372. utils.remove(elem);
  3373. }
  3374. utils.remove(under_doc_1);
  3375. utils.remove(under_doc_2);
  3376. // 执行隐藏
  3377. wk$("a[title]")[0].style.display = "none";
  3378.  
  3379. // 使文档居中
  3380. alert("建议使用:\n偏移量: 20\n缩放: 默认\n");
  3381. if (!centre(".a_left", "20")) {
  3382. return; // 如果输入非法,终止函数调用
  3383. }
  3384. // 隐藏按钮,然后打印页面
  3385. utils.print_page();
  3386. }
  3387.  
  3388.  
  3389. /**
  3390. * 阻止监听器生效
  3391. * @param {Event} e
  3392. */
  3393. function stopSpread(e) {
  3394. e.stopImmediatePropagation();
  3395. e.stopPropagation();
  3396. }
  3397.  
  3398.  
  3399. /**
  3400. * 阻止捕获事件
  3401. */
  3402. function stopCapturing() {
  3403. ["click", "mouseup"].forEach(
  3404. type => {
  3405. document.body.addEventListener(type, stopSpread, true);
  3406. document["on" + type] = undefined;
  3407. }
  3408. );
  3409. ["keypress", "keydown"].forEach(
  3410. type => {
  3411. window.addEventListener(type, stopSpread, true);
  3412. window["on" + type] = undefined;
  3413. }
  3414. );
  3415. }
  3416.  
  3417.  
  3418. /**
  3419. * 重置图像链接和最大宽度
  3420. * @param {Document} doc
  3421. */
  3422. function resetImg(doc=document) {
  3423. wk$.call(doc, "img").forEach(
  3424. elem => {
  3425. elem.style.maxWidth = "100%";
  3426. for (let attr of elem.attributes) {
  3427. if (attr.name.endsWith("-src")) {
  3428. elem.setAttribute("src", attr.value);
  3429. break;
  3430. }
  3431. }
  3432. }
  3433. );
  3434. }
  3435.  
  3436.  
  3437. /**
  3438. * 仅保留全屏文档
  3439. */
  3440. function getFullScreen() {
  3441. FullScreenObj.init();
  3442. wk$("#artContent > p:nth-child(3)")[0]?.remove();
  3443. let data = wk$("#artfullscreen__box_scr > table")[0].outerHTML;
  3444. window.doc360JS = { data };
  3445. let html_str = `
  3446. <html><head></head><body style="display: flex; flex-direction: row; justify-content: space-around">
  3447. ${data}
  3448. </body><html>
  3449. `;
  3450. wk$("html")[0].replaceWith(wk$("html")[0].cloneNode());
  3451. wk$("html")[0].innerHTML = html_str;
  3452. resetImg();
  3453. }
  3454.  
  3455.  
  3456. function cleanPage() {
  3457. getFullScreen();
  3458. stopCapturing();
  3459. }
  3460.  
  3461.  
  3462. /**
  3463. * 360doc个人图书馆下载策略
  3464. */
  3465. function doc360() {
  3466. // 创建按钮区
  3467. utils.create_btns();
  3468. // btn_1: 展开文档
  3469. utils.onclick(readAll360Doc, 1);
  3470. // btn_2: 导出纯文本
  3471. utils.onclick(saveText_360Doc, 2, "导出纯文本");
  3472. // btn_3: 打印页面到PDF
  3473. utils.onclick(printPage360Doc, 3, "打印页面到PDF");
  3474. // btn_3: 清理页面
  3475. utils.onclick(cleanPage, 4, "清理页面(推荐)");
  3476. }
  3477.  
  3478. async function getPDF() {
  3479. if (!window.DEFAULT_URL) {
  3480. alert("当前文档无法解析,请加 QQ 群反馈");
  3481. return;
  3482. }
  3483. let title = document.title.split(" - ")[0] + ".pdf";
  3484. let blob = await utils.xhr_get_blob(DEFAULT_URL);
  3485. utils.save(title, blob);
  3486. }
  3487.  
  3488.  
  3489. function mbalib() {
  3490. utils.create_btns();
  3491. utils.onclick(getPDF, 1, "下载PDF");
  3492. }
  3493.  
  3494. /**
  3495. * 判断是否进入预览模式
  3496. * @returns Boolean
  3497. */
  3498. function isInPreview() {
  3499. let p_elem = wk$("#preview_tips")[0];
  3500. if (p_elem && p_elem.style && p_elem.style.display === "none") {
  3501. return true;
  3502. }
  3503. return false;
  3504. }
  3505.  
  3506.  
  3507. /**
  3508. * 确保进入预览模式
  3509. */
  3510. async function ensureInPreview() {
  3511. while (!isInPreview()) {
  3512. // 如果没有进入预览,则先进入
  3513. if (typeof window.preview !== "function") {
  3514. alert("脚本失效,请加 QQ 群反馈");
  3515. throw new Error("preview 全局函数不存在");
  3516. }
  3517.  
  3518. await utils.sleep(500);
  3519. preview();
  3520. }
  3521. }
  3522.  
  3523.  
  3524. /**
  3525. * 前往页码
  3526. * @param {number} page_num
  3527. */
  3528. function toPage(page_num) {
  3529. // 先尝试官方接口,不行再用模拟的
  3530. try {
  3531. Viewer._GotoPage(page_num);
  3532. } catch(e) {
  3533. console.error(e);
  3534. utils.to_page(
  3535. wk$("#pageNumInput")[0],
  3536. page_num,
  3537. "keydown"
  3538. );
  3539. }
  3540. }
  3541.  
  3542.  
  3543. /**
  3544. * 展开全文预览,当展开完成后再次调用时,返回true
  3545. * @returns
  3546. */
  3547. async function walkThrough$1() {
  3548. // 隐藏页面
  3549. wk$("#pageContainer")[0].style.display = "none";
  3550.  
  3551. // 逐页加载
  3552. let lmt = window.dugenJS.lmt;
  3553. for (let i of utils.range(1, lmt + 1)) {
  3554. toPage(i);
  3555. await utils.wait_until(
  3556. () => wk$(`#outer_page_${i}`)[0].style.width.endsWith("px")
  3557. );
  3558. }
  3559.  
  3560. // 恢复显示
  3561. wk$("#pageContainer")[0].style.display = "";
  3562. console.log(`共 ${lmt} 页加载完毕`);
  3563. }
  3564.  
  3565.  
  3566. /**
  3567. * 返回当前未加载页面的页码
  3568. * @returns not_loaded
  3569. */
  3570. function getNotloadedPages() {
  3571. // 已经取得的页码
  3572. let pages = document.querySelectorAll("[id*=pageflash_]");
  3573. let loaded = new Set();
  3574. pages.forEach((page) => {
  3575. let id = page.id.split("_")[1];
  3576. id = parseInt(id);
  3577. loaded.add(id);
  3578. });
  3579. // 未取得的页码
  3580. let not_loaded = [];
  3581. for (let i of utils.range(1, window.dugenJS.lmt + 1)) {
  3582. if (!loaded.has(i)) {
  3583. not_loaded.push(i);
  3584. }
  3585. }
  3586. return not_loaded;
  3587. }
  3588.  
  3589.  
  3590. /**
  3591. * 取得全部文档页面的链接,返回urls;如果有页面未加载,则返回null
  3592. * @returns
  3593. */
  3594. function getImgUrls() {
  3595. let pages = wk$("[id*=pageflash_]");
  3596. // 尚未浏览完全部页面,返回false
  3597. if (pages.length < window.dugenJS.lmt) {
  3598. let hints = [
  3599. "尚未加载完全部页面",
  3600. "以下页面需要浏览并加载:",
  3601. getNotloadedPages().join(",")
  3602. ];
  3603. alert(hints.join("\n"));
  3604. return [false, []];
  3605. }
  3606. // 浏览完全部页面,返回urls
  3607. return [true, pages.map(page => page.querySelector("img").src)];
  3608. }
  3609.  
  3610.  
  3611. function exportImgUrls() {
  3612. let [ok, urls] = getImgUrls();
  3613. if (!ok) {
  3614. return;
  3615. }
  3616. utils.save("urls.csv", urls.join("\n"));
  3617. }
  3618.  
  3619.  
  3620. function exportPDF$1() {
  3621. let [ok, urls] = getImgUrls();
  3622. if (!ok) {
  3623. return;
  3624. }
  3625. let title = document.title.split("-")[0];
  3626. return utils.run_with_prog(
  3627. 3, () => utils.img_urls_to_pdf(urls, title)
  3628. );
  3629. }
  3630.  
  3631.  
  3632. /**
  3633. * dugen文档下载策略
  3634. */
  3635. async function dugen() {
  3636. await ensureInPreview();
  3637. // 全局对象
  3638. window.dugenJS = {
  3639. lmt: window.lmt ? window.lmt : 20
  3640. };
  3641.  
  3642. // 创建按钮区
  3643. utils.create_btns();
  3644.  
  3645. // 绑定监听器
  3646. // 按钮1:展开文档
  3647. utils.onclick(walkThrough$1, 1, "加载可预览页面");
  3648. // 按钮2:导出图片链接
  3649. utils.onclick(exportImgUrls, 2, "导出图片链接");
  3650. utils.toggle_btn(2);
  3651. // 按钮3:导出PDF
  3652. utils.onclick(exportPDF$1, 3, "导出PDF");
  3653. utils.toggle_btn(3);
  3654. }
  3655.  
  3656. // 域名级全局常量
  3657. const img_tasks = [];
  3658.  
  3659.  
  3660. /**
  3661. * 取得文档类型
  3662. * @returns {String} 文档类型str
  3663. */
  3664. function getDocType() {
  3665. const
  3666. // ["icon", "icon-format", "icon-format-doc"]
  3667. elem = wk$(".title .icon.icon-format")[0],
  3668. // "icon-format-doc"
  3669. cls = elem.classList[2];
  3670. return cls.split("-")[2];
  3671. }
  3672.  
  3673.  
  3674. /**
  3675. * 判断文档类型是否为type_list其中之一
  3676. * @returns 是否为type
  3677. */
  3678. function isTypeof(type_list) {
  3679. const type = getDocType();
  3680. if (type_list.includes(type)) {
  3681. return true;
  3682. }
  3683. return false;
  3684. }
  3685.  
  3686.  
  3687. /**
  3688. * 判断文档类型是否为PPT
  3689. * @returns 是否为PPT
  3690. */
  3691. function is_ppt() {
  3692. return isTypeof(["ppt", "pptx"]);
  3693. }
  3694.  
  3695.  
  3696. /**
  3697. * 判断文档类型是否为Excel
  3698. * @returns 是否为Excel
  3699. */
  3700. function is_excel() {
  3701. return isTypeof(["xls", "xlsm", "xlsx"]);
  3702. }
  3703.  
  3704.  
  3705. /**
  3706. * 取得未加载页面的页码
  3707. * @returns {Array} not_loaded 未加载页码列表
  3708. */
  3709. function getNotLoaded() {
  3710. const loaded = wk$("[data-id] img[src]").map(
  3711. img => parseInt(
  3712. img.closest("[data-id]").getAttribute("data-id")
  3713. )
  3714. );
  3715. return Array.from(
  3716. utils.diff(
  3717. utils.range(1, window.book118JS.page_counts + 1),
  3718. loaded
  3719. )
  3720. );
  3721. }
  3722.  
  3723.  
  3724. /**
  3725. * 取得全部文档页的url
  3726. * @returns [<是否全部加载>, <urls列表>, <未加载页码列表>]
  3727. */
  3728. function getUrls() {
  3729. const urls = wk$("[data-id] img[src]").map(
  3730. img => img.src
  3731. );
  3732. // 如果所有页面加载完毕
  3733. if (urls.length === book118JS.page_counts) {
  3734. return [true, urls, []];
  3735. }
  3736. // 否则收集未加载页面的url
  3737. return [false, urls, getNotLoaded()];
  3738. }
  3739.  
  3740.  
  3741. /**
  3742. * 展开全文
  3743. */
  3744. async function walkThrough() {
  3745. // 遍历期间隐藏按钮区
  3746. utils.toggle_box();
  3747.  
  3748. // 取得总页码
  3749. // preview.getPage()
  3750. // {current: 10, actual: 38, preview: 38, remain: 14}
  3751. const { preview: all } = preview.getPage();
  3752. for (let i = 1; i <= all; i++) {
  3753. // 逐页加载
  3754. preview.jump(i);
  3755. await utils.wait_until(
  3756. () => wk$(`[data-id="${i}"] img`)[0].src, 1000
  3757. );
  3758. }
  3759. console.log("遍历完成");
  3760. utils.toggle_box();
  3761. }
  3762.  
  3763.  
  3764. /**
  3765. * btn_2: 导出图片链接
  3766. */
  3767. function wantUrls() {
  3768. let [flag, urls, escaped] = getUrls();
  3769. // 页面都加载完毕,下载urls
  3770. if (!flag) {
  3771. // 没有加载完,提示出未加载好的页码
  3772. const hint = [
  3773. "仍有页面没有加载",
  3774. "请浏览并加载如下页面",
  3775. "是否继续导出图片链接?",
  3776. "[" + escaped.join(",") + "]"
  3777. ].join("\n");
  3778. // 终止导出
  3779. if (!confirm(hint)) {
  3780. return
  3781. }
  3782. }
  3783. utils.save("urls.csv", urls.join("\n"));
  3784. }
  3785.  
  3786.  
  3787. /**
  3788. * 打开PPT预览页面
  3789. */
  3790. async function open_iframe() {
  3791. wk$(".front a")[0].click();
  3792. const iframes = await wk$$("iframe.preview-iframe");
  3793. window.open(iframes[0].src);
  3794. }
  3795.  
  3796.  
  3797. /**
  3798. * 取得最大页码
  3799. * @returns {number} 最大页码
  3800. */
  3801. function getPageCounts$1() {
  3802. return window?.preview?.getPage()?.preview || NaN;
  3803. }
  3804.  
  3805.  
  3806. /**
  3807. * 原创力文档(非PPT或Excel)下载策略
  3808. */
  3809. async function common_doc() {
  3810. await utils.wait_until(
  3811. () => !!wk$(".counts")[0]
  3812. );
  3813.  
  3814. // 创建全局对象
  3815. window.book118JS = {
  3816. doc_type: getDocType(),
  3817. page_counts: getPageCounts$1()
  3818. };
  3819.  
  3820. // 处理非PPT文档
  3821. // 创建按钮组
  3822. utils.create_btns();
  3823. // 绑定监听器到按钮
  3824. // 按钮1:加载全文
  3825. utils.onclick(walkThrough, 1, "加载全文");
  3826. // 按钮2:导出图片链接
  3827. utils.onclick(wantUrls, 2, "导出图片链接");
  3828. utils.toggle_btn(2);
  3829. }
  3830.  
  3831.  
  3832. /**
  3833. * @returns {string}
  3834. */
  3835. function table_to_tsv() {
  3836. return wk$("table").map(table => {
  3837. // 剔除空表和行号表
  3838. const len = table.rows.length;
  3839. if (len > 1000 || len === 1) {
  3840. return "";
  3841. }
  3842.  
  3843. // 遍历行
  3844. return [...table.rows].map(row => {
  3845. // 遍历列(单元格)
  3846. return [...row.cells].map(cell => {
  3847. // 判断单元格是否存储图片
  3848. const img = cell.querySelector("img");
  3849. if (img) {
  3850. // 如果是图片,保存图片链接
  3851. return img.src;
  3852. }
  3853. // 否则保存单元格文本
  3854. return cell
  3855. .textContent
  3856. .trim()
  3857. .replace(/\n/g, " ")
  3858. .replace(/\t/g, " ");
  3859. }).join("\t");
  3860. }).join("\n").trim();
  3861. }).join("\n\n---\n\n");
  3862. }
  3863.  
  3864.  
  3865. /**
  3866. * 下载当前表格内容,保存为csv(utf-8编码)
  3867. */
  3868. function wantEXCEL() {
  3869. const tsv = table_to_tsv();
  3870. const bytes = utils.encode_to_gbk(tsv);
  3871. const fname = "原创力表格.tsv";
  3872. utils.save(fname, bytes);
  3873. }
  3874.  
  3875.  
  3876. /**
  3877. * 在Excel预览页面给出操作提示
  3878. */
  3879. function help$1() {
  3880. const hint = [
  3881. "【导出表格到TSV】只能导出当前 sheet",
  3882. "如果有多张 sheet 请在每个 sheet 上用按钮分别导出 TSV",
  3883. "TSV 文件请用记事本或 Excel 打开",
  3884. "TSV 不能存储图片,所以用图片链接代替",
  3885. "或使用此脚本复制表格到剪贴板:",
  3886. "https://greasyfork.org/zh-CN/scripts/469550",
  3887. ];
  3888. alert(hint.join("\n"));
  3889. }
  3890.  
  3891.  
  3892. /**
  3893. * 原创力文档(EXCEL)下载策略
  3894. */
  3895. function excel() {
  3896. // 创建按钮区
  3897. utils.create_btns();
  3898. // 绑定监听器到按钮
  3899. utils.onclick(wantEXCEL, 1, "导出表格到TSV");
  3900. utils.onclick(help$1, 2, "使用说明");
  3901. // 显示按钮
  3902. utils.toggle_btn(2);
  3903. }
  3904.  
  3905.  
  3906. /**
  3907. * ------------------------------ PPT 策略 ---------------------------------
  3908. */
  3909.  
  3910.  
  3911. /**
  3912. * 返回当前页码
  3913. * @returns {number}
  3914. */
  3915. function cur_page_num() {
  3916. return parseInt(
  3917. wk$("#PageIndex")[0].textContent
  3918. );
  3919. }
  3920.  
  3921.  
  3922. function add_page() {
  3923. const view = wk$("#view")[0];
  3924. view.setAttribute("style", "");
  3925.  
  3926. const i = cur_page_num() - 1;
  3927. const cur_view = wk$(`#view${i}`)[0];
  3928.  
  3929. img_tasks.push(
  3930. html2canvas(cur_view)
  3931. );
  3932. utils.btn(1).textContent = `截图: ${img_tasks.length}`;
  3933. }
  3934.  
  3935.  
  3936. function reset_tasks() {
  3937. img_tasks.splice(0);
  3938. utils.btn(1).textContent = `截图: 0`;
  3939. }
  3940.  
  3941.  
  3942. function canvas_to_blob(canvas) {
  3943. return utils.canvas_to_blob(canvas);
  3944. }
  3945.  
  3946.  
  3947. async function export_imgs_as_pdf() {
  3948. alert("正在合并截图,请耐心等待");
  3949. utils.toggle_btn(3);
  3950.  
  3951. try {
  3952. const imgs = await utils.gather(img_tasks);
  3953. const blobs = await utils.gather(
  3954. imgs.map(canvas_to_blob)
  3955. );
  3956.  
  3957. if (!blobs.length) {
  3958. alert("你尚未截取任何页面!");
  3959. } else {
  3960. await utils.img_blobs_to_pdf(blobs, "原创力幻灯片");
  3961. }
  3962. } catch(err) {
  3963. console.error(err);
  3964. }
  3965. utils.toggle_btn(3);
  3966. }
  3967.  
  3968.  
  3969.  
  3970. function ppt() {
  3971. utils.create_btns();
  3972.  
  3973. const btn1 = utils.btn(1);
  3974. btn1.onclick = add_page;
  3975. btn1.textContent = "截图当前页面";
  3976.  
  3977. utils.onclick(reset_tasks, 2, "清空截图");
  3978. utils.onclick(export_imgs_as_pdf, 3, "合并为PDF");
  3979.  
  3980. utils.toggle_btn(2);
  3981. utils.toggle_btn(3);
  3982. }
  3983.  
  3984.  
  3985. /**
  3986. * 原创力文档下载策略
  3987. */
  3988. function book118() {
  3989. const host = window.location.hostname;
  3990.  
  3991. if (host === 'max.book118.com') {
  3992. if (is_excel()) {
  3993. utils.create_btns();
  3994. utils.onclick(open_iframe, 1, "访问EXCEL");
  3995. } else if (is_ppt()) {
  3996. utils.create_btns();
  3997. utils.onclick(open_iframe, 1, "访问PPT");
  3998. } else {
  3999. common_doc();
  4000. }
  4001. } else if (wk$("#ppt")[0]) {
  4002. if (window.top !== window) return;
  4003. ppt();
  4004. } else if (wk$(`[src*="excel.min.js"]`)[0]) {
  4005. excel();
  4006. } else {
  4007. console.log(`wk: Unknown host: ${host}`);
  4008. }
  4009. }
  4010.  
  4011. // test url: https://openstd.samr.gov.cn/bzgk/gb/newGbInfo?hcno=E86BBCE32DA8E67F3DA04ED98F2465DB
  4012.  
  4013.  
  4014. /**
  4015. * 绘制0x0的bmp, 作为请求失败时返回的page
  4016. * @returns {Promise<ImageBitmap>} blank_page
  4017. */
  4018. async function blankBMP() {
  4019. let canvas = document.createElement("canvas");
  4020. [canvas.width, canvas.height] = [0, 0];
  4021. return createImageBitmap(canvas);
  4022. }
  4023.  
  4024.  
  4025. /**
  4026. * resp导出bmp
  4027. * @param {string} page_url
  4028. * @param {Promise<Response> | ImageBitmap} pms_or_bmp
  4029. * @returns {Promise<ImageBitmap>} page
  4030. */
  4031. async function respToPage(page_url, pms_or_bmp) {
  4032. let center = globalThis.gb688JS;
  4033. // 此时是bmp
  4034. if (pms_or_bmp instanceof ImageBitmap) {
  4035. return pms_or_bmp;
  4036. }
  4037.  
  4038. // 第一次下载, 且无人处理
  4039. if (!center.pages_status.get(page_url)) {
  4040. // 处理中, 设为占用
  4041. center.pages_status.set(page_url, 1);
  4042.  
  4043. // 处理
  4044. let resp;
  4045. try {
  4046. resp = await pms_or_bmp;
  4047. } catch(err) {
  4048. console.log("下载页面失败");
  4049. console.error(err);
  4050. return blankBMP();
  4051. }
  4052.  
  4053. let page_blob = await resp.blob();
  4054. let page = await createImageBitmap(page_blob);
  4055. center.pages.set(page_url, page);
  4056. // 处理结束, 设为释放
  4057. center.pages_status.set(page_url, 0);
  4058. return page;
  4059. }
  4060.  
  4061. // 有人正在下载且出于处理中
  4062. while (center.pages_status.get(page_url)) {
  4063. await utils.sleep(500);
  4064. }
  4065. return center.pages.get(page_url);
  4066. }
  4067.  
  4068.  
  4069. /**
  4070. * 获得PNG页面
  4071. * @param {string} page_url
  4072. * @returns {Promise<ImageBitmap>} bmp
  4073. */
  4074. async function getPage(page_url) {
  4075. // 如果下载过, 直接返回缓存
  4076. let pages = globalThis.gb688JS.pages;
  4077. if (pages.has(page_url)) {
  4078. return respToPage(page_url, pages.get(page_url));
  4079. }
  4080.  
  4081. // 如果从未下载过, 就下载
  4082. let resp = fetch(page_url, {
  4083. "headers": {
  4084. "accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
  4085. "accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
  4086. "proxy-connection": "keep-alive"
  4087. },
  4088. "referrer": location.href,
  4089. "referrerPolicy": "strict-origin-when-cross-origin",
  4090. "body": null,
  4091. "method": "GET",
  4092. "mode": "cors",
  4093. "credentials": "include"
  4094. });
  4095. pages.set(page_url, resp);
  4096. return respToPage(page_url, resp);
  4097. }
  4098.  
  4099.  
  4100. /**
  4101. * 返回文档页div的裁切和粘贴位置信息: [[cut_x, cut_y, paste_x%, paset_y%],...]
  4102. * @param {HTMLDivElement} page_div 文档页元素
  4103. * @returns {Array<Array<number>>} positions
  4104. */
  4105. function getPostions(page_div) {
  4106. let positions = [];
  4107.  
  4108. Array.from(page_div.children).forEach(span => {
  4109. // 'pdfImg-3-8' -> {left: 30%; top: 80%;}
  4110. let paste_pos = span.className.split("-").slice(1).map(
  4111. v => parseInt(v) / 10
  4112. );
  4113. // '-600px 0px' -> [600, 0]
  4114. let cut_pos = span.style.backgroundPosition.split(" ").map(
  4115. v => Math.abs(parseInt(v))
  4116. );
  4117. positions.push([...cut_pos, ...paste_pos]);
  4118. });
  4119. return positions;
  4120. }
  4121.  
  4122.  
  4123. /**
  4124. * 取得文档页的图像url
  4125. * @param {HTMLDivElement} page_div
  4126. * @returns {string} url
  4127. */
  4128. function getPageURL(page_div) {
  4129. // 拿到目标图像url
  4130. let path = location.pathname.split("/").slice(0, -1).join("/");
  4131. let prefix = location.origin + path + "/";
  4132. let url = page_div.getAttribute("bg");
  4133. if (!url) {
  4134. // 'url("viewGbImg?fileName=VS72l67k0jw5g3j0vErP8DTsnWvk5QsqnNLLxaEtX%2FM%3D")'
  4135. url = page_div.children[0].style.backgroundImage.split('"')[1];
  4136. }
  4137. return prefix + url;
  4138. }
  4139.  
  4140.  
  4141. /**
  4142. * 下载目标图像并拆解重绘, 返回canvas
  4143. * @param {number} i 第 i 页 (从0开始)
  4144. * @param {HTMLDivElement} page_div
  4145. * @returns {Promise<Array>} [页码, Canvas]
  4146. */
  4147. async function getAndDrawPage(i, page_div) {
  4148. // 拿到目标图像
  4149. let url = getPageURL(page_div);
  4150. let page = await getPage(url);
  4151.  
  4152. // 绘制空白A4纸背景
  4153. let [page_w, page_h] = [1190, 1680];
  4154. let bg = document.createElement("canvas");
  4155. bg.width = page_w; // 注意canvas作为取景框的大小
  4156. bg.height = page_h; // 如果不设置等于一个很小的取景框
  4157. let bg_ctx = bg.getContext("2d");
  4158. bg_ctx.fillStyle = "white";
  4159. bg_ctx.fillRect(0, 0, page_w, page_h);
  4160.  
  4161. // 逐个区块剪切取出并粘贴
  4162. // wk$("#viewer .page").forEach(page_div => {
  4163. getPostions(page_div).forEach(pos => {
  4164. bg_ctx.drawImage(
  4165. page, // image source
  4166. pos[0], // source x
  4167. pos[1], // source y
  4168. 120, // source width
  4169. 169, // source height
  4170. pos[2] * page_w, // destination x = left: x%
  4171. pos[3] * page_h, // destination y = top: y%
  4172. 120, // destination width
  4173. 169 // destination height
  4174. );
  4175. });
  4176. // });
  4177. return [i, bg];
  4178. }
  4179.  
  4180.  
  4181. /**
  4182. * 页面批量请求、裁剪重绘, 合成PDF并下载
  4183. */
  4184. async function turnPagesToPDF() {
  4185. // 渲染每页
  4186. const tasks = wk$("#viewer .page").map(
  4187. (page_div, i) => getAndDrawPage(i, page_div)
  4188. );
  4189. // 等待每页渲染完成后,排序
  4190. const results = await utils.gather(tasks);
  4191. results.sort((prev, next) => prev[0] - next[0]);
  4192. // 合并为PDF并导出
  4193. return utils.imgs_to_pdf(
  4194. results.map(item => item[1]),
  4195. // '在线预览|GB 14023-2022'
  4196. document.title.split("|")[1]
  4197. );
  4198. }
  4199.  
  4200.  
  4201. /**
  4202. * 提示预估下载耗时,然后下载
  4203. */
  4204. function hintThenDownload$1() {
  4205. // '/93'
  4206. let page_num = parseInt(wk$("#numPages")[0].textContent.slice(1));
  4207. let estimate = Math.ceil(page_num / 3);
  4208. alert(`页数: ${page_num},预计花费: ${estimate}秒;如遇网络异常可能更久\n请勿反复点击按钮;如果无法导出请 QQ 群反馈`);
  4209. turnPagesToPDF();
  4210. }
  4211.  
  4212.  
  4213. /**
  4214. * gb688文档下载策略
  4215. */
  4216. async function gb688() {
  4217. // 创建全局对象
  4218. globalThis.gb688JS = {
  4219. pages: new Map(), // {url: bmp}
  4220. pages_status: new Map() // {url: 0或1} 0释放, 1占用
  4221. };
  4222.  
  4223. // 创建按钮区
  4224. utils.create_btns();
  4225. // 绑定监听器
  4226. // 按钮1:导出PDF
  4227. utils.onclick(hintThenDownload$1, 1, "导出PDF");
  4228. }
  4229.  
  4230. function getPageCounts() {
  4231. // " / 39"
  4232. const counts_str = wk$(".counts")[0].textContent.split("/")[1];
  4233. const counts = parseInt(counts_str);
  4234. return counts > 20 ? 20 : counts;
  4235. }
  4236.  
  4237.  
  4238. /**
  4239. * 返回图片基础路径
  4240. * @returns {string} base_url
  4241. */
  4242. function getImgBaseURL() {
  4243. return wk$("#dp")[0].value;
  4244. }
  4245.  
  4246.  
  4247. function* genImgURLs$1() {
  4248. let counts = getPageCounts();
  4249. let base_url = getImgBaseURL();
  4250. for (let i = 1; i <= counts; i++) {
  4251. yield base_url + `${i}.gif`;
  4252. }
  4253. }
  4254.  
  4255.  
  4256. /**
  4257. * 下载图片,转为canvas,合并为PDF并下载
  4258. * @returns {Promise<void>}
  4259. */
  4260. function fetchThenExportPDF() {
  4261. // db2092-2014-河北特种设备使用安全管理规范_安全文库网safewk.com
  4262. let title = document.title.split("_")[0];
  4263. return utils.img_urls_to_pdf(genImgURLs$1(), title);
  4264. }
  4265.  
  4266.  
  4267. /**
  4268. * 提示预估下载耗时,然后下载
  4269. */
  4270. function hintThenDownload() {
  4271. let hint = [
  4272. "只能导出可预览的页面(最多20页)",
  4273. "请勿短时间反复点击按钮,导出用时大约不到 10 秒",
  4274. "点完后很久没动静请至 QQ 群反馈"
  4275. ];
  4276. alert(hint.join("\n"));
  4277. return utils.run_with_prog(
  4278. 1, fetchThenExportPDF
  4279. );
  4280. }
  4281.  
  4282.  
  4283. /**
  4284. * safewk文档下载策略
  4285. */
  4286. async function safewk() {
  4287. // 创建按钮区
  4288. utils.create_btns();
  4289. // 绑定监听器
  4290. // 按钮1:导出PDF
  4291. utils.onclick(
  4292. hintThenDownload, 1, "导出PDF"
  4293. );
  4294. }
  4295.  
  4296. /**
  4297. * 跳转到页码
  4298. * @param {string | number} num
  4299. */
  4300. function _to_page(num) {
  4301. if (window.WebPreview
  4302. && WebPreview.Page
  4303. && WebPreview.Page.jump
  4304. ) {
  4305. WebPreview.Page.jump(parseInt(num));
  4306. } else {
  4307. console.error("window.WebPreview.Page.jump doesn't exist");
  4308. }
  4309. }
  4310.  
  4311.  
  4312. /**
  4313. * 跳转页码GUI版
  4314. */
  4315. function to_page() {
  4316. let num = prompt("请输入要跳转的页码")?.trim();
  4317. if (/^[0-9]+$/.test(num)) {
  4318. _to_page(num);
  4319. } else {
  4320. console.log(`输入值 [${num}] 不是合法整数`);
  4321. }
  4322. }
  4323.  
  4324.  
  4325. function capture_urls() {
  4326. if (!confirm(
  4327. "只能导出已经预览页面的链接,是否继续?"
  4328. )) return;
  4329.  
  4330. let imgs = wk$("[data-id] img");
  4331. if (imgs.length === 0) {
  4332. imgs = wk$("img[data-page]");
  4333. }
  4334. console.log(imgs);
  4335.  
  4336. const urls = imgs.map(img => {
  4337. const src = img.dataset.src || img.src;
  4338. if (!src) return;
  4339. return src.startsWith("//") ? "https:" + src : src
  4340. });
  4341. const lacked = [];
  4342. const existed = urls.filter((url, i) => {
  4343. if (url) return true;
  4344. lacked.push(i + 1);
  4345. });
  4346.  
  4347. utils.save_urls(existed);
  4348. alert(
  4349. `已经浏览的页面中有 ${lacked.length} 页图片尚未加载,` +
  4350. `已经从结果中剔除。\n它们的页码是:\n${lacked}`
  4351. );
  4352. }
  4353.  
  4354.  
  4355. function* genImgURLs() {
  4356. const params = window?.previewParams;
  4357. if (!params) throw new Error(
  4358. "接口为空: window.previewParams"
  4359. );
  4360.  
  4361. let i = -4;
  4362. const
  4363. base = "https://openapi.renrendoc.com/preview/getPreview?",
  4364. query = {
  4365. temp_view: 0,
  4366. jsoncallback: "a",
  4367. callback: "b",
  4368. encrypt: params.encrypt,
  4369. doc_id: params.doc_id,
  4370. get _() { return Date.now() },
  4371. get start() { return i += 5; },
  4372. };
  4373. while (true) {
  4374. const keys = Reflect.ownKeys(query);
  4375. yield base + keys.map(
  4376. key => `${key}=${query[key]}`
  4377. ).join("&");
  4378. }
  4379. }
  4380.  
  4381.  
  4382. async function _fetch_preview_urls() {
  4383. let
  4384. is_empty = true,
  4385. switch_counts = 0,
  4386. previews = [];
  4387. for (const [i, url] of utils.enumerate(genImgURLs())) {
  4388. const resp = await fetch(url);
  4389. utils.raise_for_status(resp);
  4390. const raw_data = await resp.text(),
  4391. data = raw_data.slice(2, -1),
  4392. img_urls = JSON
  4393. .parse(data)
  4394. .data
  4395. ?.preview_list
  4396. ?.map(pair => pair.url);
  4397. if (!img_urls) break;
  4398.  
  4399. previews = previews.concat(...img_urls);
  4400. utils.update_popup(`已经请求 ${i + 1} 组图片链接`);
  4401. if (is_empty !== (img_urls.length ? false : true)) {
  4402. is_empty = !is_empty;
  4403. switch_counts++;
  4404. }
  4405. if (switch_counts === 2) break;
  4406.  
  4407. await utils.sleep(1000);
  4408. }
  4409. const
  4410. params = window.previewParams,
  4411. free = params.freepage || 20,
  4412. base = params.pre || wk$(".page img")[0].src.slice(0, -5),
  4413. free_urls = Array.from(
  4414. utils.range(1, free + 1)
  4415. ).map(
  4416. n => `${base}${n}.gif`
  4417. );
  4418.  
  4419. const urls = free_urls.concat(...previews);
  4420. utils.save_urls(urls);
  4421. }
  4422.  
  4423.  
  4424. function fetch_preview_urls() {
  4425. return utils.run_with_prog(
  4426. 3, _fetch_preview_urls
  4427. );
  4428. }
  4429.  
  4430.  
  4431. function help() {
  4432. alert(
  4433. "【捕获】和【请求】图片链接的区别:\n" +
  4434. " - 【捕获】是从当前已经加载的文档页中提取图片链接\n" +
  4435. " - 【请求】是使用官方接口直接下载图片链接\n" +
  4436. " - 【捕获】使用麻烦,但是稳定\n" +
  4437. " - 【请求】使用简单,速度快,但可能失效"
  4438. );
  4439. }
  4440.  
  4441.  
  4442. /**
  4443. * 人人文档下载策略
  4444. */
  4445. async function renrendoc() {
  4446. utils.create_btns();
  4447. utils.onclick(to_page, 1, "跳转到页码");
  4448. utils.onclick(capture_urls, 2, "捕获图片链接");
  4449. utils.onclick(fetch_preview_urls, 3, "请求图片链接");
  4450. utils.onclick(help, 4, "使用说明");
  4451.  
  4452. utils.toggle_btn(2);
  4453. utils.toggle_btn(3);
  4454. utils.toggle_btn(4);
  4455. }
  4456.  
  4457. /**
  4458. * 取得全部图片连接
  4459. * @returns {Array<string>}
  4460. */
  4461. function get_img_urls() {
  4462. const src = wk$("#page1 img")[0]?.src;
  4463.  
  4464. // 适用于图片类型
  4465. if (src) {
  4466. const path = src.split("?")[0].split("/").slice(3, -1).join("/");
  4467. const origin = new URL(location.href).origin;
  4468. const urls = window.htmlConfig.fliphtml5_pages.map(obj => {
  4469. const fname = obj.n[0].split("?")[0].split("/").at(-1);
  4470. return `${origin}/${path}/${fname}`;
  4471. });
  4472. const unique = [...new Set(urls)];
  4473. window.img_urls = unique;
  4474. return unique;
  4475. }
  4476.  
  4477. // 适用于其他类型
  4478. const relative_path = wk$(".side-image img")[0].getAttribute("src").split("?")[0];
  4479. // ../files/large/
  4480. const relative_dir = relative_path.split("/").slice(0, -1).join("/") + "/";
  4481.  
  4482. const base = location.href;
  4483. const urls = window.htmlConfig.fliphtml5_pages.map(obj => {
  4484. // "../files/large/d8b6c26f987104455efb3ec5addca7c9.jpg"
  4485. const path = relative_dir + obj.n[0].split("?")[0];
  4486. const url = new URL(path, base);
  4487. // https://book.yunzhan365.com/mctl/itid/files/large/d8b6c26f987104455efb3ec5addca7c9.jpg
  4488. return url.href.replace("/thumb/", "/content-page/");
  4489. });
  4490.  
  4491. window.img_urls = urls;
  4492. return urls;
  4493. }
  4494.  
  4495.  
  4496. function imgs_to_pdf() {
  4497. const urls = get_img_urls();
  4498. const title = document.title;
  4499. const task = () => utils.img_urls_to_pdf(urls, title);
  4500.  
  4501. utils.run_with_prog(1, task);
  4502. alert(
  4503. "正在下载图片,请稍等,时长取决于图片数量\n" +
  4504. "如果导出的文档只有一页空白页,说明当前文档不适用"
  4505. );
  4506. }
  4507.  
  4508.  
  4509. /**
  4510. * 将数组中的连续数字描述为字符串
  4511. * 例如 [1, 2, 3, 5] => "1 - 3, 5"
  4512. * @param {number[]} nums 整数数组
  4513. * @returns {string} 描述数组的字符串
  4514. */
  4515. function describe_nums(nums) {
  4516. let result = "";
  4517. let start = nums[0];
  4518. let end = nums[0];
  4519. for (let i = 1; i < nums.length; i++) {
  4520. if (nums[i] === end + 1) {
  4521. end = nums[i];
  4522. } else {
  4523. if (start === end) {
  4524. result += start + ", ";
  4525. } else {
  4526. result += start + " - " + end + ", ";
  4527. }
  4528. start = nums[i];
  4529. end = nums[i];
  4530. }
  4531. }
  4532. if (start === end) {
  4533. result += start;
  4534. } else {
  4535. result += start + " - " + end;
  4536. }
  4537. return result;
  4538. }
  4539.  
  4540.  
  4541. /**
  4542. * 取得总页码(作为str)
  4543. * @returns {string}
  4544. */
  4545. function get_total() {
  4546. const total = window?.bookConfig?.totalPageCount;
  4547. if (total) {
  4548. return String(total);
  4549. }
  4550. return wk$("#tfPageIndex input")[0].value.split("/")[1].trim();
  4551. }
  4552.  
  4553.  
  4554. /**
  4555. * 下载稀疏数组的pdf数据,每个元素应该是 [pdf_blob, pwd_str]
  4556. * @param {Array} pdfs_data
  4557. */
  4558. async function data_to_zip(pdfs_data) {
  4559. // 导入jszip
  4560. await utils.blobs_to_zip([], "empty", "dat", "empty", false);
  4561.  
  4562. // 分装截获的数据
  4563. const page_nums = Object.keys(pdfs_data)
  4564. .map(index => parseInt(index) + 1);
  4565. const len = page_nums.length;
  4566. const pwds = new Array(len + 1);
  4567. pwds[0] = "page-num,password";
  4568. // 创建压缩包,归档加密的PDF页面
  4569. const zip = new window.JSZip();
  4570. const total = get_total();
  4571. const digits = total.length;
  4572.  
  4573. // 归档
  4574. for (let i = 0; i < len; i++) {
  4575. // 页码左侧补零
  4576. const page_no = page_nums[i];
  4577. const page_no_str = page_no.toString().padStart(digits, "0");
  4578. // 记录密码
  4579. pwds[i+1] = `${page_no_str},${pdfs_data[page_no - 1][1]}`;
  4580. // 添加pdf内容到压缩包
  4581. const blob = pdfs_data[page_no - 1][0];
  4582. zip.file(`page-${page_no_str}.pdf`, blob, { binary: true });
  4583. }
  4584. console.log("zip:", zip);
  4585.  
  4586. // 添加密码本到压缩包
  4587. const pwds_blob = new Blob([pwds.join("\n")], { type: "text/plain" });
  4588. zip.file(`密码本.txt`, pwds_blob, { binary: true });
  4589. // 下载
  4590. console.info("正在合成压缩包并导出,请耐心等待几分钟......");
  4591. const zip_blob = await zip.generateAsync({ type: "blob" });
  4592. utils.save(`${document.title}.zip`, zip_blob, "application/zip");
  4593. }
  4594.  
  4595.  
  4596. /**
  4597. * 下载多个pdf为一个压缩包,其中包含一个密码本
  4598. * @param {PointerEvent} event
  4599. */
  4600. async function export_zip(event) {
  4601. // 异常判断
  4602. if (!window.pdfs_data) utils.raise(`pdfs_data 不存在!`);
  4603.  
  4604. // 确认是否继续导出PDF
  4605. const page_nums = Object.keys(pdfs_data)
  4606. .map(index => parseInt(index) + 1);
  4607. const donwload = confirm(
  4608. `已经捕获 ${page_nums.length} 个页面,是否导出?\n` +
  4609. `已捕获的页码:${describe_nums(page_nums)}\n` +
  4610. `(如果某页缺失可以先多向后翻几页,然后翻回来,来重新加载它)`
  4611. );
  4612. if (!donwload) return;
  4613. // 隐藏按钮
  4614. const btn = event.target;
  4615. btn.style.display = "none";
  4616.  
  4617. // 下载压缩包
  4618. await data_to_zip(pdfs_data);
  4619.  
  4620. // 显示按钮
  4621. btn.style.display = "block";
  4622. }
  4623.  
  4624.  
  4625. function steal_pdf_when_page_loaded() {
  4626. // 共用变量
  4627. // 存放pdf数据,[[<pdf_blob>, <pwd_str>], ...]
  4628. window.pdfs_data = [];
  4629. // 代表当前页码
  4630. let page_no = NaN;
  4631.  
  4632. // hook PdfLoadingTask.prototype.start
  4633. const _start = PdfLoadingTask.prototype.start;
  4634. wk$._start = _start;
  4635. PdfLoadingTask.prototype.start = function() {
  4636. // 取得页码
  4637. page_no = this.index;
  4638.  
  4639. // 如果不存在此页,则准备捕获此页面
  4640. if (!pdfs_data[page_no - 1]) {
  4641. pdfs_data[page_no - 1] = [];
  4642. }
  4643. return _start.call(this);
  4644. };
  4645.  
  4646. // hook getBlob
  4647. const _get_blob = getBlob;
  4648. wk$._get_blob = _get_blob;
  4649. window.getBlob = async function(param) {
  4650. const result = await _get_blob.call(this, param);
  4651. // 如果当前页面需要捕获,则设置对应项的密码
  4652. if (page_no > 0) {
  4653. const resp = await fetch(result.url);
  4654. const blob = await resp.blob();
  4655.  
  4656. pdfs_data[page_no - 1] = [blob, result.password];
  4657. page_no = NaN;
  4658. }
  4659. return result;
  4660. };
  4661.  
  4662. utils.onclick(export_zip, 1, "导出PDF压缩包");
  4663. }
  4664.  
  4665.  
  4666. /**
  4667. * 请求 url 并将资源转为 [pdf_blob, password_str]
  4668. * @param {string} url
  4669. * @returns {Array}
  4670. */
  4671. async function url_to_item(url) {
  4672. // 取得pdf数据
  4673. const resp = await fetch(url);
  4674. const buffer = await resp.arrayBuffer();
  4675. const bytes = new Uint8Array(buffer);
  4676. const len = bytes.length;
  4677.  
  4678. // 更新进度
  4679. window.downloaded_count++;
  4680. window.downloaded_size += len;
  4681. console.log(
  4682. `已经下载了 ${downloaded_count} 页,\n` +
  4683. `累计下载了 ${(downloaded_size / 1024 / 1024).toFixed(1)} MB`
  4684. );
  4685.  
  4686. // 取出密钥
  4687. const pwd = new Uint8Array(6);
  4688. pwd.set(bytes.subarray(1080, 1083));
  4689. pwd.set(bytes.subarray(-1003, -1000), 3);
  4690. const pwd_str = new TextDecoder().decode(pwd);
  4691.  
  4692. // 解密出数据
  4693. const pdf = bytes.subarray(1083, -1003);
  4694. pdf.subarray(0, 4000).forEach((byte, i) => {
  4695. pdf[i] = 255 - byte;
  4696. });
  4697. return [
  4698. new Blob([pdf, pdf.subarray(4000)], { type: "application/pdf" }),
  4699. pwd_str
  4700. ];
  4701. }
  4702.  
  4703.  
  4704. /**
  4705. * 直接下载并解析原始数据,导出PDF压缩包
  4706. * @param {PointerEvent} event
  4707. */
  4708. async function donwload_zip(event) {
  4709. // 隐藏按钮
  4710. const btn = event.target;
  4711. btn.style.display = "none";
  4712. // 共用进度变量
  4713. window.downloaded_count = 0;
  4714. window.downloaded_size = 0;
  4715.  
  4716. // 取得数据地址
  4717. const urls = get_img_urls()
  4718. .map(url => url.replace("/thumb/", "/content-page/"));
  4719. // 批量下载
  4720. const item_tasks = urls.map(url_to_item);
  4721. const items = await utils.gather(item_tasks);
  4722. // 导出ZIP
  4723. await data_to_zip(items);
  4724.  
  4725. // 显示按钮
  4726. btn.style.display = "block";
  4727. }
  4728.  
  4729.  
  4730. /**
  4731. * 导出图片到PDF
  4732. */
  4733. function judge_file_type() {
  4734. const ext = window
  4735. ?.htmlConfig
  4736. ?.fliphtml5_pages[0]
  4737. ?.n[0]
  4738. ?.split("?")[0]
  4739. ?.split(".").at(-1);
  4740.  
  4741. console.log("ext:", ext);
  4742.  
  4743. if (["zip"].includes(ext)
  4744. && window?.PdfLoadingTask
  4745. && window?.getBlob) {
  4746.  
  4747. utils.onclick(steal_pdf_when_page_loaded, 1, "开始捕获");
  4748. utils.onclick(donwload_zip, 2, "下载PDF压缩包");
  4749. utils.toggle_btn(2);
  4750. }
  4751. else if (wk$("#page1 img")[0]) {
  4752. utils.onclick(imgs_to_pdf, 1, "导出PDF");
  4753. }
  4754. else {
  4755. utils.onclick(() => null, 1, "此文档不适用");
  4756. }
  4757. }
  4758.  
  4759.  
  4760. /**
  4761. * 云展网文档下载策略
  4762. */
  4763. async function yunzhan365() {
  4764. // 根据网址分别处理
  4765. if (location.pathname.startsWith("/basic")) {
  4766. return;
  4767. }
  4768.  
  4769. // 创建脚本启动按钮
  4770. utils.create_btns();
  4771. judge_file_type();
  4772. }
  4773.  
  4774. /**
  4775. * 导出图片链接
  4776. */
  4777. function exportURLs$1() {
  4778. const all = parseInt(
  4779. wk$("[class*=total]")[0]
  4780. );
  4781. const imgs = wk$("[class*=imgContainer] img");
  4782. const got = imgs.length;
  4783.  
  4784. if (got < all) {
  4785. if (!confirm(
  4786. `当前浏览页数:${got},总页数:${all}\n建议浏览剩余页面以导出全部链接\n是否继续导出链接?`
  4787. )) {
  4788. return;
  4789. }
  4790. }
  4791. utils.save_urls(
  4792. imgs.map(img => img.src)
  4793. );
  4794. }
  4795.  
  4796.  
  4797. /**
  4798. * 360文库文档下载策略
  4799. */
  4800. function wenku360() {
  4801. utils.create_btns();
  4802. utils.onclick(
  4803. exportURLs$1, 1, "导出图片链接"
  4804. );
  4805.  
  4806. // utils.onclick(
  4807. // callAgent, 2, "导出PDF"
  4808. // );
  4809. // utils.toggle_btn(2);
  4810. }
  4811.  
  4812. async function getFileInfo() {
  4813. const
  4814. uid = new URL(location.href).searchParams.get("contentUid"),
  4815. resp = await fetch("https://zyjy-resource.webtrn.cn/sdk/api/u/open/getResourceDetail", {
  4816. "headers": {
  4817. "accept": "application/json, text/javascript, */*; q=0.01",
  4818. "content-type": "application/json",
  4819. },
  4820. "referrer": "https://jg.class.com.cn/",
  4821. "body": `{"params":{"contentUid":"${uid}"}}`,
  4822. "method": "POST",
  4823. }),
  4824. data = await resp.json(),
  4825. url = data["data"]["downloadUrl"],
  4826. fname = data["data"]["title"];
  4827.  
  4828. let ext;
  4829. try {
  4830. // validate the URL format
  4831. // and get the file format
  4832. ext = new URL(url).pathname.split(".").at(-1);
  4833. } catch(e) {
  4834. console.log(data);
  4835. throw new Error("API changed, the script is invalid now.");
  4836. }
  4837. return { url, fname, ext };
  4838. }
  4839.  
  4840.  
  4841. /**
  4842. * 保存文件
  4843. * @param {{fname: string, url: string, ext: string}} info
  4844. */
  4845. async function saveFile(info) {
  4846. const
  4847. resp = await fetch(info.url),
  4848. blob = await resp.blob();
  4849. utils.save(info.fname + `.${info.ext}`, blob);
  4850. }
  4851.  
  4852.  
  4853. /**
  4854. * 劫持保存网页,改为保存文件
  4855. * @param {KeyboardEvent} e
  4856. */
  4857. function onCtrlS(e) {
  4858. if (e.code === "KeyS" &&
  4859. e.ctrlKey) {
  4860. console.log("ctrl + s is captured!!");
  4861. getFileInfo().then(info => saveFile(info));
  4862.  
  4863. e.preventDefault();
  4864. e.stopImmediatePropagation();
  4865. e.stopPropagation();
  4866. }
  4867. }
  4868.  
  4869.  
  4870. /**
  4871. * 技工教育网文档策略
  4872. */
  4873. function jg() {
  4874. window.addEventListener(
  4875. "keydown", onCtrlS, true
  4876. );
  4877. }
  4878.  
  4879. async function estimateTimeCost() {
  4880. wk$(".w-page").at(-1).scrollIntoView();
  4881. await utils.sleep(1000);
  4882.  
  4883. let total = wk$("#pageNumber-text")[0].textContent.split("/")[1];
  4884. total = parseInt(total);
  4885. return confirm([
  4886. "注意,一旦开始截图就无法停止,除非刷新页面。",
  4887. "浏览器窗口最小化会导致截图提前结束!",
  4888. "建议将窗口最大化,这将【显著增大清晰度和文件体积】",
  4889. `预计耗时 ${1.1 * total} 秒,是否继续?`,
  4890. ].join("\n"));
  4891. }
  4892.  
  4893.  
  4894. /**
  4895. * 逐页捕获canvas
  4896. * @returns {Promise<Array<Blob>>}
  4897. */
  4898. async function collectAll() {
  4899. const imgs = [];
  4900. let div = wk$(".w-page")[0];
  4901. let i = 0;
  4902. while (true) {
  4903. // 取得 div
  4904. const anchor = Date.now();
  4905. while (!div && (Date.now() - anchor < 1000)) {
  4906. console.log(`retry on page ${i+1}`);
  4907. await utils.sleep(200);
  4908. }
  4909. if (!div) throw new Error(
  4910. `can not fetch <div>: page ${i}`
  4911. );
  4912. // 移动到 div
  4913. div.scrollIntoView({ behavior: "smooth" });
  4914. await utils.sleep(1000);
  4915. // 取得 canvas
  4916. let canvas = wk$.call(div, "canvas")[0];
  4917. let j = 0;
  4918. while (!canvas && j < 100) {
  4919. div = div.nextElementSibling;
  4920. canvas = wk$.call(div, "canvas")[0];
  4921. j++;
  4922. }
  4923. if (!div) throw new Error(
  4924. `can not fetch <div>: page ${i}*`
  4925. );
  4926.  
  4927. // 存储 canvas
  4928. imgs.push(
  4929. await utils.canvas_to_blob(canvas)
  4930. );
  4931. console.log(`canvas stored: ${++i}`);
  4932.  
  4933. // 下一轮循环
  4934. div = div.nextElementSibling;
  4935. if (!div) break;
  4936. }
  4937. console.log("done");
  4938. return imgs;
  4939. }
  4940.  
  4941.  
  4942. /**
  4943. * 放大或缩小文档画面
  4944. * @param {boolean} up
  4945. */
  4946. async function scale(up) {
  4947. let s = "#magnifyBtn";
  4948. if (!up) {
  4949. s = "#shrinkBtn";
  4950. }
  4951. const btn = wk$(s)[0];
  4952. for (let _ of utils.range(10)) {
  4953. btn.click();
  4954. await utils.sleep(500);
  4955. }
  4956. }
  4957.  
  4958.  
  4959. /**
  4960. * 获取全部canvas,显示功能按钮
  4961. * @returns
  4962. */
  4963. async function prepare() {
  4964. if (! await estimateTimeCost()) {
  4965. return;
  4966. }
  4967.  
  4968. // 隐藏按钮
  4969. utils.toggle_btn(1);
  4970. // 放大画面
  4971. await scale(true);
  4972.  
  4973. let imgs;
  4974. try {
  4975. imgs = await collectAll();
  4976. } catch(e) {
  4977. console.error(e);
  4978. } finally {
  4979. // 缩小画面
  4980. scale(false);
  4981. }
  4982. // window.imgs = imgs;
  4983. // 显示功能按钮
  4984. const fname = "技工教育网文档";
  4985. utils.onclick(
  4986. () => utils.img_blobs_to_pdf(imgs, fname),
  4987. 2,
  4988. "导出PDF"
  4989. );
  4990. utils.toggle_btn(2);
  4991.  
  4992. utils.onclick(
  4993. () => utils.blobs_to_zip(imgs, "page", "png", fname),
  4994. 3,
  4995. "导出ZIP"
  4996. );
  4997. utils.toggle_btn(3);
  4998. }
  4999.  
  5000.  
  5001. /**
  5002. * 技工教育文档预览页面策略
  5003. */
  5004. function jgPreview() {
  5005. utils.create_btns();
  5006. utils.onclick(
  5007. prepare, 1, "截图文档"
  5008. );
  5009. }
  5010.  
  5011. /**
  5012. * 取得文档标题
  5013. * @returns {string}
  5014. */
  5015. function getTitle() {
  5016. return document.title.slice(0, -4);
  5017. }
  5018.  
  5019.  
  5020. /**
  5021. * 取得基础URL
  5022. * @returns {string}
  5023. */
  5024. function getBaseURL$1() {
  5025. return wk$("#dp")[0].value;
  5026. }
  5027.  
  5028.  
  5029. /**
  5030. * 获取总页码
  5031. * @returns {number}
  5032. */
  5033. function getTotalPageNum() {
  5034. const num = wk$(".shop3 > li:nth-child(3)")[0]
  5035. .textContent
  5036. .split("/")[1]
  5037. .trim();
  5038. return parseInt(num);
  5039. }
  5040.  
  5041.  
  5042. /**
  5043. * 返回图片链接生成器
  5044. * @param {string} base 基础图片链接地址
  5045. * @param {number} max 最大数量
  5046. * @returns {Generator<string, void, unknown>}
  5047. */
  5048. function* imgURLsMaker(base, max) {
  5049. for (let i of utils.range(1, max + 1)) {
  5050. yield `${base}${i}.gif`;
  5051. }
  5052. }
  5053.  
  5054.  
  5055. /**
  5056. * 取得当前页面全部图片链接(生成器)
  5057. * @returns {Generator<string, void, unknown>}
  5058. */
  5059. function getImgURLs() {
  5060. const
  5061. base = getBaseURL$1(),
  5062. total = getTotalPageNum();
  5063. return imgURLsMaker(base, total)
  5064. }
  5065.  
  5066.  
  5067. function exportPDF() {
  5068. const urls = getImgURLs();
  5069. const title = getTitle();
  5070. return utils.run_with_prog(
  5071. 2, () => utils.img_urls_to_pdf(urls, title)
  5072. );
  5073. }
  5074.  
  5075.  
  5076. function exportURLs() {
  5077. const urls = getImgURLs();
  5078. utils.save_urls(urls);
  5079. }
  5080.  
  5081.  
  5082. /**
  5083. * 文库吧文档下载策略
  5084. */
  5085. function wenkub() {
  5086. utils.create_btns();
  5087. utils.onclick(
  5088. exportURLs, 1, "导出图片链接"
  5089. );
  5090.  
  5091. utils.onclick(
  5092. exportPDF, 2, "导出PDF(测试)"
  5093. );
  5094. utils.toggle_btn(2);
  5095. }
  5096.  
  5097. function* pageURLGen() {
  5098. const
  5099. url = new URL(location.href),
  5100. params = url.searchParams,
  5101. base = url.origin + (window.basePath || "/manuscripts/pdf"),
  5102. type = params.get("type") || "pdf",
  5103. id = params.get("id")
  5104. || new URL(wk$("#pdfContent")[0].src).searchParams.get("id")
  5105. || utils.raise("书本ID未知");
  5106. let i = 0;
  5107. let cur_url = "";
  5108. if (window.wk_sklib_url) {
  5109. console.log(`sklib 使用自定义 url: ${window.wk_sklib_url}`);
  5110.  
  5111. while (true) {
  5112. cur_url = window.wk_sklib_url.replace("{id}", id).replace("{index}", `${i}`);
  5113. yield [i, cur_url];
  5114. console.log("wk: target:", cur_url);
  5115. i++;
  5116. }
  5117. } else {
  5118. while (true) {
  5119. cur_url = `${base}/data/${type}/${id}/${i}?random=null`;
  5120. yield [i, cur_url];
  5121. console.log("wk: target:", cur_url);
  5122. i++;
  5123. }
  5124. }
  5125.  
  5126. }
  5127.  
  5128.  
  5129. async function get_bookmarks() {
  5130. const url = new URL(location.origin);
  5131. const id = utils.get_param("id");
  5132. url.pathname = `/manuscripts/pdf/catalog/pdf/${id}`;
  5133. const resp = await fetch(url.href);
  5134. const data = await resp.json();
  5135. const bookmarks = JSON.parse(data.data).outline;
  5136. return bookmarks;
  5137. }
  5138.  
  5139.  
  5140. async function save_bookmarks() {
  5141. const bookmarks = await get_bookmarks();
  5142. const text = JSON.stringify(bookmarks, null, 2);
  5143. utils.save("bookmarks.json", text, { type: "application/json" });
  5144. }
  5145.  
  5146.  
  5147. /**
  5148. * 下载所有pdf文件数据,返回字节串数组
  5149. * @returns {Promise<Array<Uint8Array>>}
  5150. */
  5151. async function fetch_all_pdfs() {
  5152. // 如果已经下载完成,则直接返回之前的结果
  5153. if (window.download_finished) {
  5154. return window.pdfs;
  5155. }
  5156.  
  5157. // 显示进度的按钮
  5158. const prog_btn = utils.btn(3);
  5159. window.download_finished = false;
  5160.  
  5161. // 存储pdf字节串
  5162. const pdfs = [];
  5163. let
  5164. last_digest = NaN,
  5165. size = NaN;
  5166.  
  5167. // 读取每个PDF的页数
  5168. if (window.loadPdfInfo) {
  5169. try {
  5170. const resp = await loadPdfInfo();
  5171. const info = JSON.parse(resp.data);
  5172. size = parseInt(info.size) || size;
  5173. } catch(e) {
  5174. console.error(e);
  5175. }
  5176. }
  5177.  
  5178. for (const [i, url] of pageURLGen()) {
  5179. // 取得数据
  5180. const b64_data = await fetch(url).then(resp => resp.text());
  5181. // 如果获取完毕,则退出
  5182. if (!b64_data.length) break;
  5183. // 计算摘要
  5184. const digest = utils.crc32(b64_data);
  5185. // 如果摘要重复了,说明到达最后一页,退出
  5186. if (digest === last_digest) break;
  5187. // 否则继续
  5188. last_digest = digest;
  5189. pdfs.push(
  5190. utils.b64_to_bytes(b64_data)
  5191. );
  5192.  
  5193. // 更新进度
  5194. const progress = `已经获取 ${i + 1} 组页面,每组`
  5195. + (size ? ` ${size} 页` : '页数未知');
  5196. console.info(progress);
  5197. prog_btn.textContent = `${i + 1} / ${size} 页`;
  5198. }
  5199.  
  5200. window.pdfs = pdfs;
  5201. window.download_finished = true;
  5202. return pdfs;
  5203. }
  5204.  
  5205.  
  5206. /**
  5207. * @param {Function} async_fn
  5208. * @returns {Function}
  5209. */
  5210. function toggle_dl_btn_wrapper(async_fn) {
  5211. return async function(...args) {
  5212. utils.toggle_btn(1);
  5213. utils.toggle_btn(2);
  5214. await async_fn(...args);
  5215. utils.toggle_btn(1);
  5216. utils.toggle_btn(2);
  5217. }
  5218. }
  5219.  
  5220.  
  5221. async function download_pdf$1() {
  5222. alert(
  5223. "如果看不到进度条请使用开发者工具(F12)查看日志\n" +
  5224. "如果文档页数过多可能导致合并PDF失败\n" +
  5225. "此时请使用【下载PDF数据集】按钮"
  5226. );
  5227.  
  5228. const pdfs = await fetch_all_pdfs();
  5229. const combined = await utils.join_pdfs(pdfs);
  5230. utils.save(
  5231. document.title + ".pdf",
  5232. combined,
  5233. "application/pdf"
  5234. );
  5235. utils.btn(3).textContent = "进度条";
  5236. }
  5237.  
  5238. download_pdf$1 = toggle_dl_btn_wrapper(download_pdf$1);
  5239.  
  5240.  
  5241. async function download_data_bundle() {
  5242. alert(
  5243. "下载的是 <文档名称>.dat 数据集\n" +
  5244. "等价于若干 PDF 的文件顺序拼接\n" +
  5245. "请使用工具切割并合并为一份 PDF\n" +
  5246. "工具(pdfs-merger)链接在脚本主页"
  5247. );
  5248.  
  5249. const pdfs = await fetch_all_pdfs();
  5250. const blob = new Blob(pdfs, { type: "application/octet-stream" });
  5251. const url = URL.createObjectURL(blob);
  5252. const a = document.createElement("a");
  5253. a.download = document.title + ".dat";
  5254. a.href = url;
  5255. a.click();
  5256.  
  5257. URL.revokeObjectURL(url);
  5258. console.log("pdf数据集", blob);
  5259. }
  5260.  
  5261. download_data_bundle = toggle_dl_btn_wrapper(download_data_bundle);
  5262.  
  5263.  
  5264. function sdlib() {
  5265. const url = new URL(location.href);
  5266. const encrypted_id = url.pathname.split("/")[2];
  5267. window.basePath = `/https/${encrypted_id}${basePath}`;
  5268. }
  5269.  
  5270.  
  5271. /**
  5272. * 钩子函数,启动于主函数生效时,便于不同网站微调
  5273. */
  5274. function load_hooks() {
  5275. const host_to_fn = {
  5276. "gwfw.sdlib.com": sdlib,
  5277. };
  5278. const fn = host_to_fn[location.hostname];
  5279. if (fn) {
  5280. // 如果存在对应 hook 函数,则调用,否则忽略
  5281. fn();
  5282. }
  5283. }
  5284.  
  5285.  
  5286. /**
  5287. * 中国社会科学文库文档策略
  5288. */
  5289. function sklib() {
  5290. // 如果存在 pdf iframe 则在 iframe 中调用自身
  5291. const iframe = wk$("iframe#pdfContent")[0];
  5292. if (iframe) return;
  5293. // 加载钩子,方便适应不同网站
  5294. load_hooks();
  5295.  
  5296. // 创建按钮区
  5297. utils.create_btns();
  5298. // 设置功能
  5299. utils.onclick(download_pdf$1, 1, "下载PDF");
  5300. utils.onclick(download_data_bundle, 2, "下载PDF数据集");
  5301. utils.onclick(() => false, 3, "进度条");
  5302. utils.onclick(save_bookmarks, 4, "下载书签");
  5303. // 显示按钮
  5304. utils.toggle_btn(2);
  5305. utils.toggle_btn(3);
  5306. utils.toggle_btn(4);
  5307. // 设置按钮样式
  5308. utils.btn(3).style.pointerEvents = "none";
  5309. }
  5310.  
  5311. /**
  5312. * 返回基础图片地址,接上 <页码>.gif 即为完整URL
  5313. * @returns {string}
  5314. */
  5315. function getBaseURL() {
  5316. const
  5317. elem = wk$("#page_1 img")[0],
  5318. src = elem.src;
  5319.  
  5320. if (!src) {
  5321. alert("当前页面不能解析!");
  5322. return;
  5323. }
  5324. if (!src.endsWith("1.gif")) {
  5325. alert("当前文档不能解析!");
  5326. throw new Error("第一页图片不以 1.gif 结尾");
  5327. }
  5328. return src.slice(0, -5);
  5329. }
  5330.  
  5331.  
  5332. function* imgURLGen() {
  5333. const
  5334. base = getBaseURL(),
  5335. max = parseInt(
  5336. // ' / 23 '
  5337. wk$(".counts")[0].textContent.split("/")[1]
  5338. );
  5339.  
  5340. for (const i of utils.range(1, max + 1)) {
  5341. yield `${base}${i}.gif`;
  5342. }
  5343. }
  5344.  
  5345.  
  5346. function getURLs() {
  5347. utils.save_urls(
  5348. imgURLGen()
  5349. );
  5350. }
  5351.  
  5352.  
  5353. function jinchutou() {
  5354. utils.create_btns();
  5355. utils.onclick(
  5356. getURLs, 1, "导出图片链接"
  5357. );
  5358. }
  5359.  
  5360. // http://www.nrsis.org.cn/mnr_kfs/file/read/55806d6159b7d8e19e633f05fa62fefa
  5361.  
  5362.  
  5363. function get_pdfs() {
  5364. // 34
  5365. const size = window?.Page.size;
  5366. if (!size) utils.raise("无法确定总页码");
  5367.  
  5368. // '/mnr_kfs/file/readPage'
  5369. const path = window
  5370. ?.loadPdf
  5371. .toString()
  5372. .match(/url:'(.+?)',/)[1];
  5373. if (!path) utils.raise("无法确定PDF路径");
  5374.  
  5375. const code = location.pathname.split("/").at(-1);
  5376.  
  5377. const tasks = [...utils.range(1, size + 1)].map(
  5378. async i => {
  5379. const resp = await fetch(path + "?wk=true", {
  5380. "headers": {
  5381. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  5382. },
  5383. "body": `code=${code}&page=${i}`,
  5384. "method": "POST",
  5385. });
  5386.  
  5387. if (!resp.ok) utils.raise(`第 ${i} 页获取失败!`);
  5388. utils.update_popup(`已经获取第 ${i} 页`);
  5389.  
  5390. const b64_str = await resp.text();
  5391. return utils.b64_to_bytes(b64_str);
  5392. }
  5393. );
  5394. return utils.gather(tasks);
  5395. }
  5396.  
  5397.  
  5398. function get_title() {
  5399. return document.title.slice(0, -5);
  5400. }
  5401.  
  5402.  
  5403. function download_pdf() {
  5404. utils.run_with_prog(1, async () => {
  5405. const pdfs = await get_pdfs();
  5406. debugger;
  5407. const pdf = await utils.join_pdfs(pdfs);
  5408. utils.save(
  5409. get_title(), pdf, "application/pdf"
  5410. );
  5411. });
  5412. }
  5413.  
  5414.  
  5415. function add_style() {
  5416. const style = `
  5417. <style>
  5418. #nprogress .nprogress-spinner-icon.forbidden {
  5419. border-top-color: #b171ff;
  5420. border-left-color: #bf8aff;
  5421. animation: nprogress-spinner 2.4s linear infinite;
  5422. }
  5423. </style>
  5424. `;
  5425. document.body.insertAdjacentHTML(
  5426. "beforeend", style
  5427. );
  5428. }
  5429.  
  5430.  
  5431. function init_forbid_origin_pdf_fetch() {
  5432. console.log("hooked xhr.open");
  5433.  
  5434. // 修改转圈图标
  5435. wk$(".nprogress-spinner-icon")[0]
  5436. .classList.add("forbidden");
  5437.  
  5438. const open = XMLHttpRequest.prototype.open;
  5439.  
  5440. // 重写 XMLHttpRequest.prototype.open 方法
  5441. XMLHttpRequest.prototype.open = function() {
  5442. const args = Array.from(arguments);
  5443. const url = args[1];
  5444.  
  5445. if (!(url.includes("readPage") &&
  5446. !url.includes("wk=true")
  5447. )) return;
  5448. this.send = () => undefined;
  5449. open.apply(this, args);
  5450. };
  5451.  
  5452. return function regain_open() {
  5453. const url = new URL(location.href);
  5454. url.searchParams.set("intercept", "0");
  5455. location.assign(url.toString());
  5456. }
  5457. }
  5458.  
  5459.  
  5460. /**
  5461. * nrsis 文档策略
  5462. */
  5463. function nrsis() {
  5464. utils.create_btns();
  5465. utils.onclick(download_pdf, 1, "下载PDF");
  5466. if (!utils.get_param("intercept")) {
  5467. add_style();
  5468. const regain_open = init_forbid_origin_pdf_fetch();
  5469. utils.onclick(regain_open, 2, "恢复页面加载");
  5470. utils.toggle_btn(2);
  5471. }
  5472. }
  5473.  
  5474. // ==UserScript==
  5475. // @name 先晓书院PDF下载
  5476. // @namespace http://tampermonkey.net/
  5477. // @version 0.1
  5478. // @description 先晓书院PDF下载,仅对PDF预览有效
  5479. // @author 2690874578@qq.com
  5480. // @match https://xianxiao.ssap.com.cn/index/rpdf/read/id/*/catalog_id/0.html?file=*
  5481. // @require https://greasyfork.org/scripts/445312-wk-full-cli/code/wk-full-cli.user.js
  5482. // @icon https://www.google.com/s2/favicons?sz=64&domain=xianxiao.ssap.com.cn
  5483. // @grant none
  5484. // @run-at document-idle
  5485. // @license GPL-3.0-only
  5486. // ==/UserScript==
  5487.  
  5488.  
  5489.  
  5490. /**
  5491. * @param {number} begin
  5492. * @param {number} end
  5493. * @param {() => void} onload
  5494. * @returns {Promise<ArrayBuffer>}
  5495. */
  5496. async function fetch_file_chunk(url, begin, end, onload) {
  5497. const resp = await fetch(url, {
  5498. headers: { "Range": `bytes=${begin}-${end}` }
  5499. });
  5500. const buffer = await resp.arrayBuffer();
  5501. onload();
  5502. return buffer;
  5503. }
  5504.  
  5505.  
  5506. /**
  5507. * 取得文档 ID
  5508. * @returns {number}
  5509. */
  5510. function get_doc_id() {
  5511. const id_text = location.pathname.split("id/")[1].split("/")[0];
  5512. return parseInt(id_text);
  5513. }
  5514.  
  5515.  
  5516. /**
  5517. * @param {string} url
  5518. * @returns {Promise<number>}
  5519. */
  5520. async function get_file_size(url) {
  5521. const resp = await fetch(url, {
  5522. headers: { "Range": `bytes=0-1` }
  5523. });
  5524. const size_text = resp.headers.get("content-range").split("/")[1];
  5525. return parseInt(size_text);
  5526. }
  5527.  
  5528.  
  5529. /**
  5530. * @param {PointerEvent} event
  5531. */
  5532. async function export_pdf(event) {
  5533. const btn = event.target;
  5534.  
  5535. // 准备请求
  5536. const doc_id = get_doc_id();
  5537. const url = `https://xianxiao.ssap.com.cn/rpdf/pdf/id/${doc_id}/catalog_id/0.pdf`;
  5538. const size = await get_file_size(url);
  5539. const chunk = 65536;
  5540. const times = Math.floor(size / chunk);
  5541. // 准备进度条
  5542. let finished = 0;
  5543. const update_progress = () => {
  5544. finished++;
  5545. const loaded = ((finished * chunk) / 1024 / 1024).toFixed(2);
  5546. const text = `已下载 ${loaded} MB`;
  5547. utils.print(`chunk<${finished}>:`, text);
  5548. btn.textContent = text;
  5549. };
  5550.  
  5551. // 分片请求PDF
  5552. const tasks = [];
  5553. for (let i = 0; i < times; i++) {
  5554. tasks[i] = fetch_file_chunk(
  5555. url,
  5556. i * chunk,
  5557. (i + 1) * chunk - 1,
  5558. update_progress,
  5559. );
  5560. }
  5561.  
  5562. // 请求最后一片
  5563. const tail = size % chunk;
  5564. tasks[times] = fetch_file_chunk(
  5565. url,
  5566. size - tail,
  5567. size - 1,
  5568. update_progress,
  5569. );
  5570.  
  5571. // 等待下载完成
  5572. const buffers = await utils.gather(tasks);
  5573. utils.print("--------全部下载完成--------");
  5574. utils.print("全部数据分片:", { get data() { return buffers; } });
  5575.  
  5576. // 导出PDF
  5577. const blob = new Blob(buffers);
  5578. const fname = top.document.title.split("_")[0] + ".pdf";
  5579. utils.save(fname, blob, "application/pdf");
  5580. }
  5581.  
  5582.  
  5583. /**
  5584. * 先晓书院 文档策略
  5585. */
  5586. function xianxiao() {
  5587. utils.print("进入<先晓书院PDF下载>脚本");
  5588. utils.create_btns();
  5589. utils.onclick(export_pdf, 1, "下载PDF");
  5590. }
  5591.  
  5592. function hook_log() {
  5593. // 保证 console.log 可用性
  5594. const con = window.console;
  5595. const { log, info, warn, error } = con;
  5596.  
  5597. // 对于 console.log 能 hook 则 hook
  5598. if (Object.getOwnPropertyDescriptor(window, "console").configurable
  5599. && Object.getOwnPropertyDescriptor(con, "log").configurable) {
  5600. // 保证 console 不能被改写
  5601. Object.defineProperty(window, "console", {
  5602. get: function() { return con; },
  5603. set: function(value) {
  5604. log.call(con, "window.console 想改成", value, "?没门!");
  5605. },
  5606. enumerable: false,
  5607. configurable: false,
  5608. });
  5609.  
  5610. // 保证日志函数不被改写
  5611. const fn_map = { log, info, warn, error };
  5612. Object.getOwnPropertyNames(fn_map).forEach((prop) => {
  5613. Object.defineProperty(con, prop, {
  5614. get: function() { return fn_map[prop]; },
  5615. set: function(value) {
  5616. log.call(con, `console.${prop} 想改成`, value, "?没门!");
  5617. },
  5618. enumerable: false,
  5619. configurable: false,
  5620. });
  5621. });
  5622. }
  5623. }
  5624.  
  5625.  
  5626. /**
  5627. * 主函数:识别网站,执行对应文档下载策略
  5628. */
  5629. function main(host=null) {
  5630. // 绑定函数到全局
  5631. window.wk_main = main;
  5632.  
  5633. // 显示当前位置
  5634. host = host || location.hostname;
  5635. const url = new URL(location.href);
  5636. const params = url.searchParams;
  5637. const path = url.pathname;
  5638.  
  5639. hook_log();
  5640. console.log(`当前 host: ${host}\n当前 url: ${url.href}`);
  5641.  
  5642. if (host.includes("docin.com")) {
  5643. docin();
  5644. } else if (host === "swf.ishare.down.sina.com.cn") {
  5645. if (params.get("wk") === "true") {
  5646. ishareData2();
  5647. } else {
  5648. ishareData();
  5649. }
  5650. } else if (host.includes("ishare.iask")) {
  5651. ishare();
  5652. } else if (host === "www.deliwenku.com") {
  5653. deliwenku();
  5654. } else if (host.includes("file") && host.includes("deliwenku.com")) {
  5655. deliFile();
  5656. } else if (host === "www.doc88.com") {
  5657. doc88();
  5658. } else if (host === "www.360doc.com") {
  5659. doc360();
  5660. } else if (host === "doc.mbalib.com") {
  5661. mbalib();
  5662. } else if (host === "www.dugen.com") {
  5663. dugen();
  5664. } else if (host === "c.gb688.cn") {
  5665. gb688();
  5666. } else if (host === "www.safewk.com") {
  5667. safewk();
  5668. } else if (host.includes("book118.com")) {
  5669. book118();
  5670. } else if (host === "www.renrendoc.com") {
  5671. renrendoc();
  5672. } else if (host.includes("yunzhan365.com")) {
  5673. yunzhan365();
  5674. } else if (host === "wenku.so.com") {
  5675. wenku360();
  5676. } else if (host === "jg.class.com.cn") {
  5677. jg();
  5678. } else if (host === "preview.imm.aliyuncs.com") {
  5679. jgPreview();
  5680. } else if (host === "www.wenkub.com") {
  5681. wenkub();
  5682. } else if (
  5683. (host.includes("sklib") && path === "/manuscripts/")
  5684. || host === "gwfw.sdlib.com") {
  5685. sklib();
  5686. } else if (host === "www.jinchutou.com") {
  5687. jinchutou();
  5688. } else if (host === "www.nrsis.org.cn") {
  5689. nrsis();
  5690. } else if (host === "xianxiao.ssap.com.cn") {
  5691. xianxiao();
  5692. } else {
  5693. console.log("匹配到了无效网页");
  5694. }
  5695. }
  5696.  
  5697.  
  5698. setTimeout(main, 1000);
  5699.  
  5700. })();