keledge-helper

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

当前为 2023-07-04 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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();
})();