您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
人大论文平台下载工具,请勿传播下载的文件,否则后果自负。
// ==UserScript== // @name RUC-Thesis-Download 人大论文平台下载工具 // @namespace https://greasyfork.org/zh-CN/scripts/459341-ruc-thesis-download // @supportURL https://github.com/xiaotianxt/PKU-Thesis-Download // @homepageURL https://github.com/xiaotianxt/PKU-Thesis-Download // @version 0.2 // @description 人大论文平台下载工具,请勿传播下载的文件,否则后果自负。 // @author xiaotianxt // @match http://*.ruc.edu.cn/foxit-htmlreader-web/Reader.do* // @icon https://www.google.com/s2/favicons?sz=64&domain=ruc.edu.cn // @require https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js // @license GNU GPLv3 // @grant unsafeWindow // ==/UserScript== const RESOLUTION = "ruc_thesis_download.resolution"; const DEFAULT_RESOLUTION = '100'; (async function () { "use strict"; const message = (msg) => { const msgBox = document.getElementById("msgBox"); msgBox.textContent = msg; } const initUI = async () => waitForElm("#bmtab").then(element => { // 下载按钮 const downloadButton = element.cloneNode(true); downloadButton.innerHTML = ` <img style="width:49;height:49" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADEAAAAxCAYAAABznEEcAAAACXBIWXMAAOxgAADsYAHjlJf+AAAGkmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4wLWMwMDAgNzkuMTcxYzI3ZmFiLCAyMDIyLzA4LzE2LTIyOjM1OjQxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjQuMSAoTWFjaW50b3NoKSIgeG1wOkNyZWF0ZURhdGU9IjIwMjMtMDItMDJUMTg6NTc6MjYrMDg6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDIzLTAyLTAyVDE5OjAxOjM3KzA4OjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDIzLTAyLTAyVDE5OjAxOjM3KzA4OjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpiZmNlZDhlOC1jMTkxLTRhMTQtOTAyOS0xYjgzYzRlNTFhY2YiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MzUyNDljMWQtNjg5ZC00YWJiLTgzYjUtN2M5OGU0OWI0MzliIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MzUyNDljMWQtNjg5ZC00YWJiLTgzYjUtN2M5OGU0OWI0MzliIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDozNTI0OWMxZC02ODlkLTRhYmItODNiNS03Yzk4ZTQ5YjQzOWIiIHN0RXZ0OndoZW49IjIwMjMtMDItMDJUMTg6NTc6MjYrMDg6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNC4xIChNYWNpbnRvc2gpIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDozMjFhMGRhNS1iNmY5LTRjNzEtOGE4Ni01YjA4NDYyMjIwZTEiIHN0RXZ0OndoZW49IjIwMjMtMDItMDJUMTg6NTg6MTYrMDg6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNC4xIChNYWNpbnRvc2gpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpiZmNlZDhlOC1jMTkxLTRhMTQtOTAyOS0xYjgzYzRlNTFhY2YiIHN0RXZ0OndoZW49IjIwMjMtMDItMDJUMTk6MDE6MzcrMDg6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNC4xIChNYWNpbnRvc2gpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDwvcmRmOlNlcT4gPC94bXBNTTpIaXN0b3J5PiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pm8EcWkAAAG0SURBVGiB7Ze9Sh1BGEDPXU0kQVMIqcQ8gJ1FIGAvWCcPEMgTpPSNfAMTYpVGkk4IpEilCDZyE5QEf+I9FrNBc+P1emdnyAhzYGGH3fm+Pbs7M9/0VO47zf9+gBRUiVKoEqVQJUqhSpRClSiFKjGGd4DXjh1gLkeinBKnQ+2TXPlySlzc0M5S99cxUQpVYgznN7SPciSaThirARYJ0+gxMDt0/QmwAjwADoGv/Dv441BTHY36Rt1TP6s//JtTdVf9qb5Vp1LlTimBuqC+93a22vuS5U0tgfpM3R4h8EVdSp0zhwTqc/XbkEBffZkjXy4J1Ffq91ZgoK7nypVTAsMA/qVuqNO58vQ0qpxZa6fKBvgEHNxy72tgC9gfcf0p8ALoEdaSzYmfJtK+b5gyz+3+n6+qJ228fkyM2MXuMfCwPe+66jfAzJ93Ghsghut7ha6r7mBE3DuTonbqKtG59EhROz0C5gkDc1Ik/JpG9geInp2OuNov7xAKuqmIOBeEF7Dcto8JheJExEqcEabY1PyOiRs7Jj5E9hvHx5hOsV+iKOr2tBSqRClUiVKoEqVQJUqhSpTCJUa/YvjPuS6aAAAAAElFTkSuQmCC" > <div id="msgBox">下载</div> `; downloadButton.style.height = '70px'; downloadButton.style.color = 'white'; document.querySelector("#btnList").appendChild(downloadButton); downloadButton.addEventListener("click", downloadPDF); // 清晰度 const resolution = localStorage.getItem(RESOLUTION) || DEFAULT_RESOLUTION; const resolutionRadioGroup = downloadButton.cloneNode(true); resolutionRadioGroup.innerHTML = ` <input type="radio" name="resolution" id="standard" value="100"> <label for="standard">标清</label> <input type="radio" name="resolution" id="high" value="150"> <label for="high">超清</label> <input type="radio" name="resolution" id="super" value="200"> <label for="super">巨清</label> `; document.querySelector("#btnList").appendChild(resolutionRadioGroup); resolutionRadioGroup.querySelectorAll("input").forEach(elem => elem.addEventListener("click", (e) => { localStorage.setItem(RESOLUTION, e.target.value); rewriteFetch(); })) resolutionRadioGroup.querySelector("[value='" + resolution + "']").checked = true }) const rewriteFetch = async () => { const originOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (_, url) { if (url.includes("GetPageImg.do?")) { const epage = atob(url.split("epage=")[1]); const remain = epage.split('&zoom=')[0]; const resolution = localStorage.getItem(RESOLUTION) || DEFAULT_RESOLUTION; const newEpage = remain + "&zoom=" + resolution; const newUrl = 'GetPageImg.do?epage=' + btoa(newEpage) const newArgs = [...arguments]; newArgs[1] = newUrl; console.log('rewrite', { url, newUrl }); originOpen.apply(this, newArgs); } else { originOpen.apply(this, arguments); } }; } const downloadPDF = async () => { const totalPage = Number($("#totalPages").text().replace(/ \/ /, "")); const resolution = localStorage.getItem(RESOLUTION) || DEFAULT_RESOLUTION; const fileID = document.getElementById("fileid")?.value; const template = (page) => 'GetPageImg.do?epage=' + btoa(`fileid=${fileID}&page=${page}&zoom=${resolution}`) const urls = []; for (let page = 0; page < totalPage; page++) { urls.push(template(page)); } let cnt = 0; message(cnt + "/" + urls.length); const base64s = await parallel(urls, async (url, index) => { const tryFetch = async (attempt = 3) => { if (!attempt) return; const base64 = await fetch(url).then(res => res.blob()).then(blob => { const reader = new FileReader(); reader.readAsDataURL(blob); return new Promise((resolve) => { reader.onloadend = () => { resolve(reader.result); }; }); }) if (base64.length < 1000) { console.log(`error may occur on ${index}, try ${attempt}`); await new Promise(r => setTimeout(r, 1000)) return tryFetch(attempt - 1); } return base64; } const res = await tryFetch(); cnt += 1 message(cnt + "/" + urls.length); return res; }, 3) const doc = jspdf.jsPDF(); const canvas = document.createElement('canvas'); canvas.width = 210 * 4; canvas.height = 297 * 4; const ctx = canvas.getContext('2d'); const next = (i) => { if (i >= base64s.length) { doc.save('download.pdf'); return; } const base64 = base64s[i]; const image = new Image(); image.onload = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(image, 0, 0, canvas.width, canvas.height); doc.addImage(canvas.toDataURL('image/jpeg'), 'JPEG', 0, 0, 210, 297); doc.addPage(); next(i + 1); } image.onerror = () => { const text = `img error on page ${i + 1}, url: ${urls[i]}}` doc.text(text, 10, 10); doc.addPage(); next(i + 1); } image.src = base64; } next(0); } // wait until element exists // https://stackoverflow.com/questions/5525071/how-to-wait-until-an-element-exists const waitForElm = (selector) => { return new Promise((resolve) => { const element = document.querySelector(selector); if (element) { return resolve(element); } const observer = new MutationObserver(() => { const element = document.querySelector(selector); if (element) { resolve(element); observer.disconnect(); } }); observer.observe(document.body, { childList: true, subtree: true, }); }); } // parallel from https://zhuanlan.zhihu.com/p/360193435 const parallel = async (jobs, fn, workerCount = 5) => { const ret = new Array(jobs.length); let cursor = 0 async function worker(workerId) { let currentJob; while (cursor < jobs.length) { try { currentJob = cursor; cursor += 1; ret[currentJob] = await fn(jobs[currentJob], cursor); } catch (e) { console.error(`worker: ${workerId} job: ${currentJob}`, e) } } } const workers = [] for (let i = 0; i < workerCount && i < jobs.length; i += 1) { workers.push(worker(i)) } await Promise.all(workers); return ret; } initUI(); })();