keledge-helper

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

目前為 2023-07-04 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         keledge-helper
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  可知网导出页面到PDF,仅对PDF预览有效
// @author       [email protected]
// @match        https://www.keledge.com/pdfReader?*
// @require      https://cdn.staticfile.org/pdf-lib/1.17.1/pdf-lib.min.js
// @icon         https://www.google.com/s2/favicons?sz=64&domain=keledge.com
// @grant        none
// @run-at       document-start
// @license      GPL-3.0-only
// ==/UserScript==


(function() {
    'use strict';


    // 全局常量
    const GUI = `<div><style class="keledge-style">.keledge-fold-btn{position:fixed;left:151px;top:36%;user-select:none;font-size:large;z-index:1001}.keledge-fold-btn::after{content:"🐵"}.keledge-fold-btn.folded{left:20px}.keledge-fold-btn.folded::after{content:"🙈"}.keledge-box{position:fixed;width:154px;left:10px;top:32%;z-index:1000}.btns-sec{background:#e7f1ff;border:2px solid #1676ff;padding:0 0 10px 0;font-weight:600;border-radius:2px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Hiragino Sans GB','Microsoft YaHei','Helvetica Neue',Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol'}.btns-sec.folded{display:none}.logo-title{width:100%;background:#1676ff;text-align:center;font-size:large;color:#e7f1ff;line-height:40px;height:40px;margin:0 0 16px 0}.keledge-box button{display:block;width:128px;height:28px;border-radius:4px;color:#fff;font-size:12px;border:none;outline:0;margin:8px auto;font-weight:700;cursor:pointer;opacity:.9}.keledge-box button.folded{display:none}.keledge-box .btn-1{background:linear-gradient(180deg,#00e7f7 0,#feb800 .01%,#ff8700 100%)}.keledge-box .btn-1:hover,.keledge-box .btn-2:hover{opacity:.8}.keledge-box .btn-1:active,.keledge-box .btn-2:active{opacity:1}</style><div class="keledge-box"><section class="btns-sec"><p class="logo-title">keledge-helper</p><button class="btn-1" onclick="btn1_fn(this)">{{btn1_desc}}</button></section><p class="keledge-fold-btn" onclick="[this, this.parentElement.querySelector('.btns-sec')].forEach(elem => elem.classList.toggle('folded'))"></p></div></div>`;


    // 全局变量
    window.pdf_data_list = [];
    window.log = console.log.bind(console);
    window.error = console.error.bind(console);


    /**
     * @param {number} delay 
     */
    function sleep(delay) {
        return new Promise(resolve => setTimeout(resolve, delay));
    }

    async function wait_for_pdfjs() {
        while (!window.pdfjsLib) {
            await sleep(200);
        }
    }


    function hooked_get_doc(pdf_data) {
        pdf_data_list.push(pdf_data.data);
        log(`page collected: ${pdf_data_list.length}`);
        return getDocument(pdf_data);
    }


    function hook_pdfjs() {
        window.getDocument = pdfjsLib.getDocument.bind(pdfjsLib);
        pdfjsLib.getDocument = hooked_get_doc;
    }


    /**
     * 加载CDN脚本
     * @param {string} url 
     */
    async function load_web_script(url) {
        try {
            // xhr+eval方式
            const resp = await fetch(url);
            const code = await resp.text();
            Function(code)();
        } catch(e) {
            error(e);
            // 嵌入<script>方式
            return new Promise((resolve) => {
                const script = document.createElement("script");
                script.src = url;
                script.onload = resolve;
                document.body.append(script);
            });
        }
    }


    /**
     * 返回一个包含计数器的迭代器, 其每次迭代值为 [index, value]
     * @param {Iterable} iterable 
     * @returns 
     */
    function* enumerate(iterable) {
        let i = 0;
        for (let value of iterable) {
            yield [i, value];
            i++;
        }
    }


    /**
     * 合并多个PDF
     * @param {Array<ArrayBuffer | Uint8Array>} pdfs 
     * @returns {Promise<Uint8Array>}
     */
    async function join_pdfs(pdfs) {
        if (!window.PDFLib) {
            const url = "https://cdn.staticfile.org/pdf-lib/1.17.1/pdf-lib.min.js";
            await load_web_script(url);
        }

        const combined = await PDFLib.PDFDocument.create();

        for (const [i, buffer] of enumerate(pdfs)) {
            const pdf = await PDFLib.PDFDocument.load(buffer);
            const pages = await combined.copyPages(
                pdf, pdf.getPageIndices()
            );

            for (const page of pages) {
                combined.addPage(page);
            }
            log(`已经合并 ${i + 1} 组`);
        }

        return combined.save();
    }


    /**
     * 创建并下载文件
     * @param {string} file_name 文件名
     * @param {ArrayBuffer | ArrayBufferView | Blob | string} content 内容
     * @param {string} type 媒体类型,需要符合 MIME 标准 
     */
    function save(file_name, content, type="") {
        const blob = new Blob(
            [content], { type }
        );
        const size = (blob.size / 1024).toFixed(1);
        log(`blob saved, size: ${size} kb, type: ${blob.type}`);

        const url = URL.createObjectURL(blob);
        const a = document.createElement("a");
        a.download = file_name || "未命名文件";
        a.href = url;
        a.click();
        URL.revokeObjectURL(url);
    }


    async function export_pdf() {
        const combined = await join_pdfs(pdf_data_list);
        save(document.title + ".pdf", combined, "application/pdf");
    }


    /**
     * @param {string} selectors 
     * @returns {HTMLElement}
     */
    function $(selectors) {
        const self = this?.querySelector ? this : document;
        return self.querySelector(selectors);
    }


    /**
     * 等待直到函数返回true
     * @param {Function} is_ready 判断条件达成与否的函数
     * @param {number} timeout 最大等待秒数, 默认5000毫秒
     * @returns {Promise<boolean>} 是否在超时前返回
     */
    async function until(is_ready, timeout=5000) {
        const gap = 200;
        let chances = parseInt(timeout / gap);
        chances = chances < 1 ? 1 : chances;
        
        while (!is_ready()) {
            await sleep(200);
            chances -= 1;
            if (!chances) {
                break;
            }
        }

        if (chances === 0) {
            error(`超时!(${timeout} ms);超时函数: `, is_ready);
            return false;
        }
        return true;
    }


    /**
     * 判断指定页码的页面是否加载完成
     * @param {number} page_no 
     * @returns 
     */
    function is_page_loaded(page_no) {
        return !!$(`[id*="pdf-page-${page_no}"] [data-loaded="true"]`);
    }


    /**
     * @param {HTMLElement} element
     * @returns {Promise<boolean>} 是否被封禁
     */
    async function on_page_loaded(element) {
        const success = await until(() => $.call(element, `[data-loaded="true"]`));
        return !success;
    }


    async function main() {
        log("进入 keledge-helper 脚本");

        await wait_for_pdfjs();
        hook_pdfjs();

        window.btn1_fn = export_pdf;
        const gui = GUI.replace("{{btn1_desc}}", "导出PDF");
        document.body.insertAdjacentHTML("beforeend", gui);
    }


    main();
})();