keledge-helper

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

目前为 2023-07-04 提交的版本。查看 最新版本

// ==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();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址