Greasy Fork is available in English.

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 计量技术规范,详见下方说明。

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         Wenku Doc Downloader
// @namespace    http://tampermonkey.net/
// @version      1.10.1
// @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 计量技术规范,详见下方说明。
// @author       2690874578@qq.com
// @match        *://*.docin.com/p-*
// @match        *://docimg1.docin.com/?wk=true
// @match        *://ishare.iask.sina.com.cn/f/*
// @match        *://ishare.iask.com/f/*
// @match        *://swf.ishare.down.sina.com.cn/?path=*
// @match        *://swf.ishare.down.sina.com.cn/?wk=true
// @match        *://www.deliwenku.com/p-*
// @match        *://file.deliwenku.com/?num=*
// @match        *://file3.deliwenku.com/?num=*
// @match        *://www.doc88.com/p-*
// @match        *://www.360doc.com/content/*
// @match        *://doc.mbalib.com/view/*
// @match        *://www.dugen.com/p-*
// @match        *://max.book118.com/html/*
// @match        *://openapi.book118.com/?*
// @match        *://view-cache.book118.com/pptView.html?*
// @match        *://*.book118.com/?readpage=*
// @match        *://c.gb688.cn/bzgk/gb/showGb?*
// @match        *://www.safewk.com/p-*
// @match        *://www.renrendoc.com/paper/*
// @match        *://www.renrendoc.com/p-*
// @match        *://www.yunzhan365.com/basic/*
// @match        *://book.yunzhan365.com/*index.html*
// @match        *://wenku.so.com/d/*
// @match        *://jg.class.com.cn/cms/resourcedetail.htm?contentUid=*
// @match        *://preview.imm.aliyuncs.com/index.html?url=*/jgjyw/*
// @match        *://www.wenkub.com/p-*.html*
// @match        *://*/manuscripts/?*
// @match        *://gwfw.sdlib.com:8000/*
// @match        *://www.jinchutou.com/shtml/view-*
// @match        *://www.jinchutou.com/p-*
// @match        *://www.nrsis.org.cn/*/read/*
// @match        https://xianxiao.ssap.com.cn/readerpdf/?id=*
// @match        https://xianxiao.ssap.com.cn/index/rpdf/read/id/*/catalog_id/0.html?file=*
// @require      https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/jspdf/2.5.1/jspdf.umd.min.js
// @require      https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/html2canvas/1.4.1/html2canvas.min.js
// @icon         https://s2.loli.net/2022/01/12/wc9je8RX7HELbYQ.png
// @icon64       https://s2.loli.net/2022/01/12/tmFeSKDf8UkNMjC.png
// @grant        none
// @run-at       document-idle
// @license      GPL-3.0-only
// @create       2021-11-22
// @note         1. 应对 sklib 的 AES 加密
// ==/UserScript==


(function () {
    'use strict';

    /**
     * 基于 window.postMessage 通信的套接字对象
     */
    class Socket {
        /**
         * 创建套接字对象
         * @param {Window} target 目标窗口
         */
        constructor(target) {
            if (!(target.window && (target === target.window))) {
                console.log(target);
                throw new Error(`target is not a [Window Object]`); 
            }
            this.target = target;
            this.connected = false;
            this.listeners = new Set();
        }

        get [Symbol.toStringTag]() { return "Socket"; }

        /**
         * 向目标窗口发消息
         * @param {*} message 
         */
        talk(message) {
            if (!this.target) {
                throw new TypeError(
                    `socket.target is not a window: ${this.target}`
                );
            }
            this.target.postMessage(message, "*");
        }

        /**
         * 添加捕获型监听器,返回实际添加的监听器
         * @param {Function} listener (e: MessageEvent) => {...}
         * @param {boolean} once 是否在执行后自动销毁,默认 false;如为 true 则使用自动包装过的监听器
         * @returns {Function} listener
         */
        listen(listener, once=false) {
            if (this.listeners.has(listener)) {
                return;
            }

            let real_listener = listener;
            // 包装监听器
            if (once) {
                const self = this;
                function wrapped(e) {
                    listener(e);
                    self.notListen(wrapped);
                }
                real_listener = wrapped;
            }
            
            // 添加监听器
            this.listeners.add(real_listener);
            window.addEventListener(
                "message", real_listener, true
            );
            return real_listener;
        }

        /**
         * 移除socket上的捕获型监听器
         * @param {Function} listener (e: MessageEvent) => {...}
         */
        notListen(listener) {
            console.log(listener);
            console.log(
                "listener delete operation:",
                this.listeners.delete(listener)
            );
            window.removeEventListener("message", listener, true);
        }

        /**
         * 检查对方来信是否为pong消息
         * @param {MessageEvent} e 
         * @param {Function} resolve 
         */
        _on_pong(e, resolve) {
            // 收到pong消息
            if (e.data.pong) {
                this.connected = true;
                this.listeners.forEach(
                    listener => listener.ping ? this.notListen(listener) : 0
                );
                console.log("Client: Connected!\n" + new Date());
                resolve(this);
            }
        }

        /**
         * 向对方发送ping消息
         * @returns {Promise<Socket>}
         */
        _ping() {
            return new Promise((resolve, reject) => {
                // 绑定pong检查监听器
                const listener = this.listen(
                    e => this._on_pong(e, resolve)
                );
                listener.ping = true;

                // 5分钟后超时
                setTimeout(
                    () => reject(new Error(`Timeout Error during receiving pong (>5min)`)),
                    5 * 60 * 1000
                );
                // 发送ping消息
                this.talk({ ping: true });
            });
        }

        /**
         * 检查对方来信是否为ping消息
         * @param {MessageEvent} e 
         * @param {Function} resolve 
         */
        _on_ping(e, resolve) {
            // 收到ping消息
            if (e.data.ping) {
                this.target = e.source;
                this.connected = true;
                this.listeners.forEach(
                    listener => listener.pong ? this.notListen(listener) : 0
                );
                console.log("Server: Connected!\n" + new Date());
                
                // resolve 后期约状态无法回退
                // 但后续代码仍可执行
                resolve(this);
                // 回应pong消息
                this.talk({ pong: true });
            }
        }

        /**
         * 当对方来信是为ping消息时回应pong消息
         * @returns {Promise<Socket>}
         */
        _pong() {
            return new Promise(resolve => {
                // 绑定ping检查监听器
                const listener = this.listen(
                    e => this._on_ping(e, resolve)
                );
                listener.pong = true;
            });
        }

        /**
         * 连接至目标窗口
         * @param {boolean} talk_first 是否先发送ping消息
         * @param {Window} target 目标窗口
         * @returns {Promise<Socket>}
         */
        connect(talk_first) {
            // 先发起握手
            if (talk_first) {
                return this._ping();
            }
            // 后发起握手
            return this._pong();
        }
    }


    const base = {
        Socket,

        init_gbk_encoder() {

            let table;

            function initGbkTable() {
                // https://en.wikipedia.org/wiki/GBK_(character_encoding)#Encoding
                const ranges = [
                    [0xA1, 0xA9,  0xA1, 0xFE],
                    [0xB0, 0xF7,  0xA1, 0xFE],
                    [0x81, 0xA0,  0x40, 0xFE],
                    [0xAA, 0xFE,  0x40, 0xA0],
                    [0xA8, 0xA9,  0x40, 0xA0],
                    [0xAA, 0xAF,  0xA1, 0xFE],
                    [0xF8, 0xFE,  0xA1, 0xFE],
                    [0xA1, 0xA7,  0x40, 0xA0],
                ];
                const codes = new Uint16Array(23940);
                let i = 0;

                for (const [b1Begin, b1End, b2Begin, b2End] of ranges) {
                    for (let b2 = b2Begin; b2 <= b2End; b2++) {
                        if (b2 !== 0x7F) {
                            for (let b1 = b1Begin; b1 <= b1End; b1++) {
                                codes[i++] = b2 << 8 | b1;
                            }
                        }
                    }
                }
                table = new Uint16Array(65536);
                table.fill(0xFFFF);

                const str = new TextDecoder('gbk').decode(codes);
                for (let i = 0; i < str.length; i++) {
                    table[str.charCodeAt(i)] = codes[i];
                }
            }

            const defaultOnAlloc = (len) => new Uint8Array(len);
            const defaultOnError = () => 63;   // '?'

            /**
             * 字符串编码为gbk字节串
             * @param {string} str
             * @param {Function} onError 处理编码失败时返回字符替代值的函数,默认是返回 63('?') 的函数
             * @returns {Uint8Array}
             */
            return function(str, onError=null) {
                if (!table) {
                    initGbkTable();
                }
                const onAlloc = defaultOnAlloc;
                onError = onError === null ? defaultOnError : onError;

                const buf = onAlloc(str.length * 2);
                let n = 0;

                for (let i = 0; i < str.length; i++) {
                    const code = str.charCodeAt(i);
                    if (code < 0x80) {
                        buf[n++] = code;
                        continue;
                    }

                    const gbk = table[code];

                    if (gbk !== 0xFFFF) {
                        buf[n++] = gbk;
                        buf[n++] = gbk >> 8;
                    }
                    
                    else if (code === 8364) {
                        // 8364 == '€'.charCodeAt(0)
                        // Code Page 936 has a single-byte euro sign at 0x80
                        buf[n++] = 0x80;
                    }
                    
                    else {
                        const ret = onError(i, str);
                        if (ret === -1) {
                            break;
                        }
                        if (ret > 0xFF) {
                            buf[n++] = ret;
                            buf[n++] = ret >> 8;
                        } else {
                            buf[n++] = ret;
                        }
                    }
                }
                return buf.subarray(0, n)
            }
        },

        /**
         * Construct a table with table[i] as the length of the longest prefix of the substring 0..i
         * @param {Array<number>} arr 
         * @returns {Array<number>}
         */
        longest_prefix: function(arr) {

            // create a table of size equal to the length of `str`
            // table[i] will store the prefix of the longest prefix of the substring str[0..i]
            let table = new Array(arr.length);
            let maxPrefix = 0;
            // the longest prefix of the substring str[0] has length
            table[0] = 0;

            // for the substrings the following substrings, we have two cases
            for (let i = 1; i < arr.length; i++) {
                // case 1. the current character doesn't match the last character of the longest prefix
                while (maxPrefix > 0 && arr[i] !== arr[maxPrefix]) {
                    // if that is the case, we have to backtrack, and try find a character  that will be equal to the current character
                    // if we reach 0, then we couldn't find a chracter
                    maxPrefix = table[maxPrefix - 1];
                }
                // case 2. The last character of the longest prefix matches the current character in `str`
                if (arr[maxPrefix] === arr[i]) {
                    // if that is the case, we know that the longest prefix at position i has one more character.
                    // for example consider `-` be any character not contained in the set [a-c]
                    // str = abc----abc
                    // consider `i` to be the last character `c` in `str`
                    // maxPrefix = will be 2 (the first `c` in `str`)
                    // maxPrefix now will be 3
                    maxPrefix++;
                    // so the max prefix for table[9] is 3
                }
                table[i] = maxPrefix;
            }
            return table;
        },

        // 用于取得一次列表中所有迭代器的值
        getAllValus: function(iterators) {
            if (iterators.length === 0) {
                return [true, []];
            }
        
            let values = [];
            for (let iterator of iterators) {
                let {value, done} = iterator.next();
                if (done) {
                    return [true, []];
                }
                values.push(value);
            }
            return [false, values];
        },

        /**
         * 使用过时的execCommand复制文字
         * @param {string} text
         */
        oldCopy: function(text) {
            document.oncopy = function(event) {
                event.clipboardData.setData('text/plain', text);
                event.preventDefault();
            };
            document.execCommand('Copy', false, null);
        },

        b64ToUint6: function(nChr) {
            return nChr > 64 && nChr < 91 ?
                nChr - 65
                : nChr > 96 && nChr < 123 ?
                nChr - 71
                : nChr > 47 && nChr < 58 ?
                nChr + 4
                : nChr === 43 ?
                62
                : nChr === 47 ?
                63
                :
                0;
        },

        /**
         * 元素选择器
         * @param {string} selector 选择器
         * @returns {Array<HTMLElement>} 元素列表
         */
        $: function(selector) {
            const self = this?.querySelectorAll ? this : document;
            return [...self.querySelectorAll(selector)];
        },

        /**
         * 安全元素选择器,直到元素存在时才返回元素列表,最多等待5秒
         * @param {string} selector 选择器
         * @returns {Promise<Array<HTMLElement>>} 元素列表
         */
        $$: async function(selector) {
            const self = this?.querySelectorAll ? this : document;

            for (let i = 0; i < 10; i++) {
                let elems = [...self.querySelectorAll(selector)];
                if (elems.length > 0) {
                    return elems;
                }
                await new Promise(r => setTimeout(r, 500));
            }
            throw Error(`"${selector}" not found in 5s`);
        },

        /**
         * 将2个及以上的空白字符(除了换行符)替换成一个空格
         * @param {string} text 
         * @returns {string}
         */
        stripBlanks: function(text) {
            return text
                .replace(/([^\r\n])(\s{2,})(?=[^\r\n])/g, "$1 ")
                .replace(/\n{2,}/, "\n");
        },

        /**
         * 复制属性(含访问器)到 target
         * @param {Object} target 
         * @param  {...Object} sources 
         * @returns 
         */
        superAssign: function(target, ...sources) {
            sources.forEach(source => 
                Object.defineProperties(
                    target, Object.getOwnPropertyDescriptors(source)
                )
            );
            return target;
        },

        makeCRC32: function() {
            function makeCRCTable() {
                let c;
                let crcTable = [];
                for(var n =0; n < 256; n++){
                    c = n;
                    for(var k =0; k < 8; k++){
                        c = ((c&1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
                    }
                    crcTable[n] = c;
                }
                return crcTable;
            }

            const crcTable = makeCRCTable();

            /**
             * @param {string} str
             * @returns {number}
             */
            return function(str) {
                let crc = 0 ^ (-1);
            
                for (var i = 0; i < str.length; i++ ) {
                    crc = (crc >>> 8) ^ crcTable[(crc ^ str.charCodeAt(i)) & 0xFF];
                }
            
                return (crc ^ (-1)) >>> 0;
            };
        }
    };

    const box = `
<div class="wk-box">
    <section class="btns-sec">
        <p class="logo_tit">Wenku Doc Downloader</p>
        <button class="btn-1">展开文档 😈</button>
        <button class="btn-2">空按钮 2</button>
        <button class="btn-3">空按钮 3</button>
        <button class="btn-4">空按钮 4</button>
        <button class="btn-5">空按钮 5</button>
    </section>
    <p class="wk-fold-btn unfold"></p>
</div>
`;

    const style = `
<style class="wk-style">
    .wk-fold-btn {
        position: fixed;
        left: 151px;
        top: 36%;
        user-select: none;
        font-size: large;
        z-index: 1001;
    }

    .wk-fold-btn::after {
        content: "🐵";
    }
    
    .wk-fold-btn.folded {
        left: 20px;
    }
    
    .wk-fold-btn.folded::after {
        content: "🙈";
    }

    .wk-box {
        position: fixed;
        width: 154px;
        left: 10px;
        top: 32%;
        z-index: 1000;
    }

    .btns-sec {
        background: #E7F1FF;
        border: 2px solid #1676FF;
        padding: 0px 0px 10px 0px;
        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_tit {
        width: 100%;
        background: #1676FF;
        text-align: center;
        font-size: 12px;
        color: #E7F1FF;
        line-height: 40px;
        height: 40px;
        margin: 0 0 16px 0;
    }

    .btn-1 {
        display: block;
        width: 128px;
        height: 28px;
        background: linear-gradient(180deg, #00E7F7 0%, #FEB800 0.01%, #FF8700 100%);
        border-radius: 4px;
        color: #fff;
        font-size: 12px;
        border: none;
        outline: none;
        margin: 8px auto;
        font-weight: bold;
        cursor: pointer;
        opacity: .9;
    }

    .btn-2 {
        display: none;
        width: 128px;
        height: 28px;
        background: #07C160;
        border-radius: 4px;
        color: #fff;
        font-size: 12px;
        border: none;
        outline: none;
        margin: 8px auto;
        font-weight: bold;
        cursor: pointer;
        opacity: .9;
    }

    .btn-3 {
        display: none;
        width: 128px;
        height: 28px;
        background: #FA5151;
        border-radius: 4px;
        color: #fff;
        font-size: 12px;
        border: none;
        outline: none;
        margin: 8px auto;
        font-weight: bold;
        cursor: pointer;
        opacity: .9;
    }

    .btn-4 {
        display: none;
        width: 128px;
        height: 28px;
        background: #1676FF;
        border-radius: 4px;
        color: #fff;
        font-size: 12px;
        border: none;
        outline: none;
        margin: 8px auto;
        font-weight: bold;
        cursor: pointer;
        opacity: .9;
    }

    .btn-5 {
        display: none;
        width: 128px;
        height: 28px;
        background: #ff6600;
        border-radius: 4px;
        color: #fff;
        font-size: 12px;
        border: none;
        outline: none;
        margin: 8px auto;
        font-weight: bold;
        cursor: pointer;
        opacity: .9;
    }


    .btns-sec button:hover {
        opacity: 0.8;
    }

    .btns-sec button:active{
        opacity: 1;
    }

    .btns-sec button[disabled] {
        cursor: not-allowed;
        opacity: 1;
        filter: grayscale(1);
    }

    .wk-popup-container {
        height: 100vh;
        width: 100vw;
        display: flex;
        flex-direction: column;
        justify-content: space-around;
        z-index: 999;
        background: 0 0;
    }

    .wk-popup-head {
        font-size: 1.5em;
        margin-bottom: 12px
    }

    .wk-card {
        background: #fff;
        background-image: linear-gradient(48deg, #fff 0, #e5efe9 100%);
        border-top-right-radius: 16px;
        border-bottom-left-radius: 16px;
        box-shadow: -20px 20px 35px 1px rgba(10, 49, 86, .18);
        display: flex;
        flex-direction: column;
        padding: 32px;
        margin: 0;
        max-width: 400px;
        width: 100%
    }

    .content-wrapper {
        font-size: 1.1em;
        margin-bottom: 44px
    }

    .content-wrapper:last-child {
        margin-bottom: 0
    }

    .wk-button {
        align-items: center;
        background: #e5efe9;
        border: 1px solid #5a72b5;
        border-radius: 4px;
        color: #121943;
        cursor: pointer;
        display: flex;
        font-size: 1em;
        font-weight: 700;
        height: 40px;
        justify-content: center;
        width: 150px
    }

    .wk-button:focus {
        border: 2px solid transparent;
        box-shadow: 0 0 0 2px #121943;
        outline: solid 4px transparent
    }

    .link {
        color: #121943
    }

    .link:focus {
        box-shadow: 0 0 0 2px #121943
    }

    .input-wrapper {
        display: flex;
        flex-direction: column
    }

    .input-wrapper .label {
        align-items: baseline;
        display: flex;
        font-weight: 700;
        justify-content: space-between;
        margin-bottom: 8px
    }

    .input-wrapper .optional {
        color: #5a72b5;
        font-size: .9em
    }

    .input-wrapper .input {
        border: 1px solid #5a72b5;
        border-radius: 4px;
        height: 40px;
        padding: 8px
    }

    .modal-header {
        align-items: baseline;
        display: flex;
        justify-content: space-between
    }

    .close {
        background: 0 0;
        border: none;
        cursor: pointer;
        display: flex;
        height: 16px;
        text-decoration: none;
        width: 16px
    }

    .close svg {
        width: 16px
    }

    .modal-wrapper {
        background: rgba(0, 0, 0, .7);
    }

    #wk-popup {
        opacity: 0;
        transition: opacity .25s ease-in-out;
        display: none;
        flex-direction: row;
        justify-content: space-around;
    }

    #wk-popup:target {
        opacity: 1;
        display: flex;
    }

    #wk-popup:target .modal-body {
        opacity: 1;
        transform: translateY(1);
    }

    #wk-popup .modal-body {
        max-width: 500px;
        opacity: 0;
        transform: translateY(-3vh);
        transition: opacity .25s ease-in-out;
        width: 100%;
        z-index: 1
    }

    .outside-trigger {
        bottom: 0;
        cursor: default;
        left: 0;
        position: fixed;
        right: 0;
        top: 0;
    }
</style>
`;

    const popup = `
<div class="wk-popup-container">
    <div class='modal-wrapper' id='wk-popup'>
        <div class='modal-body wk-card'>
            <div class='modal-header'>
                <h2 class='wk-popup-head'>下载进度条</h2>
                <a href='#!' role='wk-button' class='close' aria-label='close this modal'>
                    <svg viewBox='0 0 24 24'>
                        <path
                            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'>
                        </path>
                    </svg>
                </a>
            </div>
            <p class='wk-popup-body'>正在初始化内容...</p>
        </div>
        <a href='#!' class='outside-trigger'></a>
    </div>
</div>
`;

    globalThis.wk$ = base.$;
    globalThis.wk$$ = base.$$;


    const utils = {
        Socket: base.Socket,

        PDF_LIB_URL: "https://cdn.staticfile.org/pdf-lib/1.17.1/pdf-lib.min.js",

        encode_to_gbk: base.init_gbk_encoder(),

        print: function(...args) {
            const time = new Date().toTimeString().slice(0, 8);
            console.info(`[wk ${time}]`, ...args);
        },

        /**
         * 字节串转b64字符串
         * @param {Uint8Array} bytes 
         * @returns {Promise<string>}
         */
        bytes_to_b64: function(bytes) {
            return new Promise((resolve, reject) => {
                const reader = new FileReader();
                reader.onerror = () => reject(new Error("转换失败", { cause: bytes }));
                reader.onloadend = () => resolve(reader.result.split(",")[1]);
                reader.readAsDataURL(new Blob([bytes]));
            });
        },

        /**
         * 以指定原因弹窗提示并抛出错误
         * @param {string} reason 
         */
        raise: function(reason) {
            alert(reason);
            throw new Error(reason);
        },

        /**
         * 将错误定位转为可读的字符串
         * @param {Error} err 
         * @returns {string}
         */
        get_stack: function(err) {
            let stack = `${err.stack}`;
            const matches = stack.matchAll(/at .+?( [(].+[)])/g);

            for (const group of matches) {
                stack = stack.replace(group[1], "");
            }
            return stack.trim();
        },

        /**
         * 合并多个PDF
         * @param {Array<ArrayBuffer | Uint8Array>} pdfs 
         * @param {Function} loop_fn
         * @param {Window} win
         * @returns {Promise<Uint8Array>}
         */
        join_pdfs: async function(pdfs, loop_fn=null, win=null) {
            const _win = win || window;
            if (!_win.PDFLib) {
                await this.load_web_script(this.PDF_LIB_URL);
            }

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

            for (const [i, buffer] of this.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);
                }

                if (loop_fn) {
                    // 如有,则使用自定义钩子函数
                    loop_fn();
                } else {
                    // 否则使用旧版 popup
                    this.update_popup(`已经合并 ${i + 1} 组`);
                }
            }

            return await combined.save();
        },

        /**
         * raise an error for status which is not in [200, 299] 
         * @param {Response} response 
         */
        raise_for_status(response) {
            if (!response.ok) {
                throw new Error(
                    `Fetch Error with status code: ${response.status}`
                );
            }
        },

        /**
         * 计算 str 的 CRC32 摘要(number)
         * @param {string} str
         * @returns {number}
         */
        crc32: base.makeCRC32(),

        /**
         * 返回函数参数定义
         * @param {Function} fn 
         * @param {boolean} print 是否打印到控制台,默认 true
         * @returns {string | undefined}
         */
        help: function(fn, print=true) {
            if (!(fn instanceof Function))
                throw new Error(`fn must be a function`);

            const
                _fn = fn.__func__ || fn,
                ARROW_ARG = /^([^(]+?)=>/,
                FN_ARGS = /^[^(]*\(\s*([^)]*)\)/m,
                STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,
                fn_text = Function.prototype.toString.call(_fn).replace(STRIP_COMMENTS, ''),
                args = fn_text.match(ARROW_ARG) || fn_text.match(FN_ARGS),
                // 如果自带 doc,优先使用,否则使用源码
                doc = fn.__doc__ ? fn.__doc__ : args[0];
            
            if (!print) return base.stripBlanks(doc);

            const color = (window.matchMedia &&
                    window.matchMedia('(prefers-color-scheme: dark)').matches
                ) ;
            console.log("%c" + doc, `color: ${color}; font: small italic`);
        },

        /**
         * 字节数组转十六进制字符串
         * @param {Uint8Array} arr 
         * @returns {string}
         */
        hex_bytes: function(arr) {
            return Array.from(arr)
                .map(byte => byte.toString(16).padStart(2, "0"))
                .join("");
        },

        /**
         * 取得对象类型
         * @param {*} obj 
         * @returns {string} class
         */
        classof: function(obj) {
            return Object
                .prototype
                .toString
                .call(obj)
                .slice(8, -1);
        },

        /**
         * 随机改变字体颜色、大小、粗细
         * @param {HTMLElement} elem 
         */
        emphasize_text: function(elem) {
            const rand = Math.random;
            elem.style.cssText = `
            font-weight: ${200 + parseInt(700 * rand())};
            font-size: ${(1 + rand()).toFixed(1)}em;
            color: hsl(${parseInt(360 * rand())}, ${parseInt(40 + 60 * rand())}%, ${parseInt(60 * rand())}%);
            background-color: yellow;`;
        },

        /**
         * 等待直到 DOM 节点停止变化
         * @param {HTMLElement} elem 监听节点 
         * @param {number} timeout 超时毫秒数
         * @returns {Promise<MutationObserver>} observer
         */
        until_stop: async function(elem, timeout=2000) {
            // 创建用于共享的监听器
            let observer;
            // 创建超时 Promise
            const timeout_promise = new Promise((_, reject) => {
                setTimeout(() => {
                    // 停止监听、释放资源
                    observer.disconnect();
                    const error = new Error(
                        `Timeout Error occured on listening DOM mutation (max ${timeout}ms)`,
                        { cause: elem }
                    );
                    reject(error);
                }, timeout);
            });
            
            // 开始元素节点变动监听
            return Promise.race([
                new Promise(resolve => {
                    // 创建监听器
                    observer = new MutationObserver(
                        (_, observer) => {
                            // DOM 变动结束后终止监听、释放资源
                            observer.disconnect();
                            // 返回监听器
                            resolve(observer);
                        }
                    );
                    // 开始监听目标节点
                    observer.observe(elem, {
                        subtree: true,
                        childList: true,
                        attributes: true
                    });
                }),
                timeout_promise,
            ])
            .catch(error => {
                if (`${error}`.includes("Timeout Error")) {
                    return observer;
                }
                console.error(error);
                throw error;
            });
        },

        /**
         * Find all the patterns that matches in a given string `str`
         * this algorithm is based on the Knuth–Morris–Pratt algorithm. Its beauty consists in that it performs the matching in O(n)
         * @param {Array<number>} arr 
         * @param {Array<number>} sub_arr 
         * @returns {Array<number>}
         */
        kmp_matching: function(arr, sub_arr) {
            // find the prefix table in O(n)
            let prefixes = base.longest_prefix(sub_arr);
            let matches = [];

            // `j` is the index in `P`
            let j = 0;
            // `i` is the index in `S`
            let i = 0;
            while (i < arr.length) {
                // Case 1.  S[i] == P[j] so we move to the next index in `S` and `P`
                if (arr[i] === sub_arr[j]) {
                    i++;
                    j++;
                }
                // Case 2.  `j` is equal to the length of `P`
                // that means that we reached the end of `P` and thus we found a match
                if (j === sub_arr.length) {
                    matches.push(i - j);
                    // Next we have to update `j` because we want to save some time
                    // instead of updating to j = 0 , we can jump to the last character of the longest prefix well known so far.
                    // j-1 means the last character of `P` because j is actually `P.length`
                    // e.g.
                    // S =  a b a b d e
                    // P = `a b`a b
                    // we will jump to `a b` and we will compare d and a in the next iteration
                    // a b a b `d` e
                    //     a b `a` b
                    j = prefixes[j - 1];
                }
                // Case 3.
                // S[i] != P[j] There's a mismatch!
                else if (arr[i] !== sub_arr[j]) {
                    // if we have found at least a character in common, do the same thing as in case 2
                    if (j !== 0) {
                        j = prefixes[j - 1];
                    } else {
                        // otherwise, j = 0, and we can move to the next character S[i+1]
                        i++;
                    }
                }
            }

            return matches;
        },

        /**
             * 用文件头切断文件集合体
             * @param {Uint8Array} bytes 
             * @param {Uint8Array} head 默认 null,即使用 data 前 8 字节
             * @returns {Array<Uint8Array>}
             */
        split_files_by_head: function(bytes, head=null) {
            const sub = bytes.subarray || bytes.slice;
            head = head || sub.call(bytes, 0, 8);
            
            const indexes = this.kmp_matching(bytes, head);
            const size = indexes.length;
            indexes.push(bytes.length);

            const parts = new Array(size);
            for (let i = 0; i < size; i++) {
                parts[i] = sub.call(bytes, indexes[i], indexes[i+1]);
            }
            // 返回结果数组
            return parts;
        },

        /**
         * 函数装饰器:仅执行一次 func
         */
        once: function(fn) {
            let used = false;
            return function() {
                if (!used) {
                    used = true;
                    return fn();
                }
            }
        },

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

        /**
         * 同步的迭代若干可迭代对象
         * @param  {...Iterable} iterables 
         * @returns 
         */
        zip: function* (...iterables) {
            // 强制转为迭代器
            let iterators = iterables.map(
                iterable => iterable[Symbol.iterator]()
            );

            // 逐次迭代
            while (true) {
                const [done, values] = base.getAllValus(iterators);
                if (done) {
                    return;
                }
                if (values.length === 1) {
                    yield values[0];
                } else {
                    yield values;
                }
            }
        },

        /**
         * 返回指定范围整数生成器
         * @param {number} end 如果只提供 end, 则返回 [0, end)
         * @param {number} end2 如果同时提供 end2, 则返回 [end, end2)
         * @param {number} step 步长, 可以为负数,不能为 0
         * @returns 
         */
        range: function*(end, end2=null, step=1) {
            // 参数合法性校验
            if (step === 0) {
                throw new RangeError("step can't be zero");
            }
            const len = end2 - end;
            if (end2 && len && step && (len * step < 0)) {
                throw new RangeError(`[${end}, ${end2}) with step ${step} is invalid`);
            }

            // 生成范围
            end2 = end2 === null ? 0 : end2;
            let [small, big] = [end, end2].sort((a, b) => a - b);
            // 开始迭代
            if (step > 0) {
                for (let i = small; i < big; i += step) {
                    yield i;
                }
            } else {
                for (let i = big; i > small; i += step) {
                    yield i;
                }
            }    },

        /**
         * 获取整个文档的全部css样式
         * @returns {string} css text
         */
        get_all_styles: function() {
            let styles = [];
            for (let sheet of document.styleSheets) {
                let rules;
                try {
                    rules = sheet.cssRules;
                } catch(e) {
                    if (!(e instanceof DOMException)) {
                        console.error(e);
                    }
                    continue;
                }

                for (let rule of rules) {
                    styles.push(rule.cssText);   
                }
            }
            return styles.join("\n\n");
        },

        /**
         * 复制text到剪贴板
         * @param {string} text 
         * @returns 
         */
        copy_text: function(text) {
            // 输出到控制台和剪贴板
            console.log(
                text.length > 20 ?
                    text.slice(0, 21) + "..." : text
            );
            
            if (!navigator.clipboard) {
                base.oldCopy(text);
                return;
            }
            navigator.clipboard
                .writeText(text)
                .catch(_ => base.oldCopy(text));
        },

        /**
         * 复制媒体到剪贴板
         * @param {Blob} blob
         */
        copy: async function(blob) {
            const data = [new ClipboardItem({ [blob.type]: blob })];
            try {
                await navigator.clipboard.write(data);
                console.log(`${blob.type} 成功复制到剪贴板`);
            } catch (err) {
                console.error(err.name, err.message);
            }
        },

        /**
         * 创建并下载文件
         * @param {string} file_name 文件名
         * @param {ArrayBuffer | ArrayBufferView | Blob | string} content 内容
         * @param {string} type 媒体类型,需要符合 MIME 标准 
         */
        save: function(file_name, content, type="") {
            if (!type && (content instanceof Blob)) {
                type = content.type;
            }

            let blob = null;
            if (content instanceof Array) {
                blob = new Blob(content, { type });
            } else {
                blob = new Blob([content], { type });
            }
            
            const size = parseInt((blob.size / 1024).toFixed(0)).toLocaleString();
            console.log(`blob saved, size: ${size} KB, type: ${blob.type}`, blob);

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

        /**
         * 显示/隐藏按钮区
         */
        toggle_box: function() {
            let sec = wk$(".wk-box")[0];
            if (sec.style.display === "none") {
                sec.style.display = "block";
                return;
            }
            sec.style.display = "none";
        },

        /**
         * 异步地睡眠 delay 毫秒, 可选 max_delay 控制波动范围
         * @param {number} delay 等待毫秒
         * @param {number} max_delay 最大等待毫秒, 默认为null
         * @returns
         */
        sleep: async function(delay, max_delay=null) {
            max_delay = max_delay === null ? delay : max_delay;
            delay = delay + (max_delay - delay) * Math.random();
            return new Promise(resolve => setTimeout(resolve, delay));
        },

        /**
         * 允许打印页面
         */
        allow_print: function() {
            const style = document.createElement("style");
            style.innerHTML = `
            @media print {
                body { display: block; }
            }`;
            document.head.append(style);
        },

        /**
         * 取得get参数key对应的value
         * @param {string} key
         * @returns {string} value
         */
        get_param: function(key) {
            return new URL(location.href).searchParams.get(key);
        },

        /**
         * 求main_set去除cut_set后的set
         * @param {Iterable} main_set 
         * @param {Iterable} cut_set 
         * @returns 差集
         */
        diff: function(main_set, cut_set) {
            const _diff = new Set(main_set);
            for (let elem of cut_set) {
                _diff.delete(elem);
            }
            return _diff;
        },

        /**
         * 增强按钮(默认为蓝色按钮:展开文档)的点击效果
         * @param {string} i 按钮序号
         */
        enhance_click: async function(i) {
            let btn = this.btn(i);
            const style = btn.getAttribute("style") || "";
            
            // 变黑缩小
            btn.setAttribute(
                "style",
                style + "color: black; font-weight: normal;"
            );
            
            await utils.sleep(500);
            btn = this.btn(i);
            // 复原加粗
            btn.setAttribute("style", style);
        },

        /**
         * 绑定事件处理函数到指定按钮,返回实际添加的事件处理函数
         * @param {(event: PointerEvent) => Promise<void>} listener click监听器
         * @param {number} i 按钮序号
         * @param {string} new_text 按钮的新文本,为null则不替换
         * @returns {Function} 事件处理函数
         */
        onclick: function(listener, i, new_text=null) {
            const btn = this.btn(i);

            // 如果需要,替换按钮内文本
            if (new_text) {
                btn.textContent = new_text;
            }

            // 绑定事件,添加到页面上
            /**
             * @param {PointerEvent} event 
             */
            async function wrapped_listener(event) {
                const btn = event.target;
                const text = btn.textContent;
                btn.disabled = true;
                try {
                    await listener.call(btn, event);
                } catch(err) {
                    console.error(err);
                }
                btn.disabled = false;
                btn.textContent = text;
            }

            btn.onclick = wrapped_listener;
            return wrapped_listener;
        },

        /**
         * 返回第 index 个按钮引用
         * @param {number} i 
         * @returns {HTMLButtonElement}
         */
        btn: function(i) {
            return wk$(`.wk-box [class="btn-${i}"]`)[0];
        },

        /**
         * 强制隐藏元素
         * @param {string | Array<HTMLElement>} selector_or_elems 
         */
        force_hide: function(selector_or_elems) {
            const cls = "force-hide";
            const elems = selector_or_elems instanceof Array ?
                selector_or_elems : wk$(selector_or_elems);

            elems.forEach(elem => {
                elem.classList.add(cls);
            });

            // 判断css样式是否已经存在
            let style = wk$(`style.${cls}`)[0];
            // 如果已经存在,则无须重复创建
            if (style) {
                return;
            }
            // 否则创建
            style = document.createElement("style");
            style.innerHTML = `style.${cls} {
            visibility: hidden !important;
            display: none !important;
        }`;
            document.head.append(style);
        },

        /**
         * 等待直到元素可见。最多等待5秒。
         * @param {HTMLElement} elem 一个元素
         * @returns {Promise<HTMLElement>} elem
         */
        until_visible: async function(elem) {
            let [max, i] = [25, 0];
            let style = getComputedStyle(elem);
            // 如果不可见就等待0.2秒/轮
            while (i <= max &&
                (style.display === "none" ||
                style.visibility !== "hidden")
                ) {
                i++;
                style = getComputedStyle(elem);
                await this.sleep(200);
            }
            return elem;
        },

        /**
         * 等待直到函数返回true
         * @param {Function} isReady 判断条件达成与否的函数
         * @param {number} timeout 最大等待秒数, 默认5000毫秒
         */
        wait_until: async function(isReady, timeout=5000) {
            const gap = 200;
            let chances = parseInt(timeout / gap);
            chances = chances < 1 ? 1 : chances;
            
            while (! await isReady()) {
                await this.sleep(200);
                chances -= 1;
                if (!chances) {
                    break;
                }
            }
        },

        /**
         * 隐藏按钮,打印页面,显示按钮
         */
        print_page: function() {
            // 隐藏按钮,然后打印页面
            this.toggle_box();
            setTimeout(window.print, 500);
            setTimeout(this.toggle_box, 1000);
        },

        /**
         * 切换按钮显示/隐藏状态
         * @param {number} i 按钮序号
         * @returns 按钮元素的引用
         */
        toggle_btn: function(i) {
            const btn = this.btn(i);
            const display = getComputedStyle(btn).display;
            
            if (display === "none") {
                btn.style.display = "block";
            } else {
                btn.style.display = "none";
            }
            return btn;
        },

        /**
         * 用input框跳转到对应页码
         * @param {HTMLInputElement} input 当前页码
         * @param {string | number} page_num 目标页码
         * @param {string} type 键盘事件类型:"keyup" | "keypress" | "keydown"
         */
        to_page: function(input, page_num, type) {
            // 设置跳转页码为目标页码
            input.value = `${page_num}`;
            // 模拟回车事件来跳转
            const enter = new KeyboardEvent(type, {
                bubbles: true,
                cancelable: true,
                keyCode: 13
            });
            input.dispatchEvent(enter);
        },

        /**
         * 判断给定的url是否与当前页面同源
         * @param {string} url 
         * @returns {boolean}
         */
        is_same_origin: function(url) {
            url = new URL(url);
            if (url.protocol === "data:") {
                return true;
            }
            if (location.protocol === url.protocol
                && location.host === url.host
                && location.port === url.port
                ) {
                return true;
            }
            return false;
        },

        /**
         * 在新标签页打开链接,如果提供文件名则下载
         * @param {string} url 
         * @param {string} fname 下载文件的名称,默认为空,代表不下载
         */
        open_in_new_tab: function(url, fname="") {
            const a = document.createElement("a");
            a.href = url;
            a.target = "_blank";
            if (fname && this.is_same_origin(url)) {
                a.download = fname;
            }
            a.click();
        },

        /**
         * 用try移除元素
         * @param {HTMLElement | string} elem_or_selector
         */
        remove: function(elem_or_selector) {
            try {
                const cls = this.classof(elem_or_selector);
                if (cls === "String") {
                    wk$(elem_or_selector).forEach(
                        elem => elem.remove()
                    );
                }
                else if (cls.endsWith("Element")) {
                    elem_or_selector.remove();
                }
            } catch (e) {
                console.error(e);
            }
        },

        /**
         * 用try移除若干元素
         * @param {Iterable<HTMLElement>} elements 要移除的元素列表
         */
        remove_multi: function(elements) {
            for (const elem of elements) {
                this.remove(elem);
            }
        },

        /**
         * 等待全部任务落定后返回值的列表
         * @param {Array<Promise>} tasks 
         * @returns {Promise<Array>}
         */
        gather: async function(tasks) {
            const results = await Promise.allSettled(tasks);
            const values = [];

            for (const result of results) {
                // 期约成功解决且返回值不为空的才有效
                if (result.status === "fulfilled"
                    && !([NaN, null, undefined].includes(result.value))) {
                    values.push(result.value);
                }
            }
            return values;
        },

        /**
         * html元素列表转为canvas列表
         * @param {Array<HTMLElement>} elements 
         * @returns {Promise<Array<HTMLCanvasElement>>}
         */
        elems_to_canvases: async function(elements) {
            if (!globalThis.html2canvas) {
                await this.load_web_script(
                    "https://cdn.staticfile.org/html2canvas/1.4.1/html2canvas.min.js"
                );
            }

            // 如果是空列表, 则抛出异常
            if (elements.length === 0) {
                throw new Error("htmlToCanvases 未得到任何html元素");
            }

            return this.gather(
                elements.map(html2canvas)
            );
        },

        /**
         * 将html元素转为canvas再合并到pdf中,最后下载pdf
         * @param {Array<HTMLElement>} elements 元素列表
         * @param {string} title 文档标题
         */
        elems_to_pdf: async function(elements, title="文档") {
            // 如果是空元素列表,终止函数
            const canvases = await this.elems_to_canvases(elements);
            // 控制台检查结果
            console.log("生成的canvas元素如下:");
            console.log(canvases);
            // 合并为PDF
            this.imgs_to_pdf(canvases, title);
        },

        /**
         * 使用xhr异步GET请求目标url,返回响应体blob
         * @param {string} url 
         * @returns {Promise<Blob>} blob
         */
        xhr_get_blob: async function(url) {
            const xhr = new XMLHttpRequest();
            xhr.open("GET", url);
            xhr.responseType = "blob";
            
            return new Promise((resolve, reject) => {
                xhr.onload = () => {
                    const code = xhr.status;
                    if (code >= 200 && code <= 299) {
                        resolve(xhr.response);
                    }
                    else {
                        reject(new Error(`Network Error: ${code}`));
                    }
                };
                xhr.send();
            });
        },

        /**
         * 加载CDN脚本
         * @param {string} url 
         */
        load_web_script: async function(url) {
            try {
                const resp = await fetch(url);
                const code = await resp.text();
                Function(code)();

            } catch(e) {
                console.error(e);
                // 嵌入<script>方式
                return new Promise(resolve => {
                    const script = document.createElement("script");
                    script.src = url;
                    script.onload = resolve;
                    document.body.append(script);
                });
            }
        },

        /**
         * b64编码字符串转Uint8Array
         * @param {string} sBase64 b64编码的字符串
         * @param {number} nBlockSize 字节数
         * @returns {Uint8Array} arr
         */
        b64_to_bytes: function(sBase64, nBlockSize=1) {
            const
                sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length,
                nOutLen = nBlockSize ? Math.ceil((nInLen * 3 + 1 >>> 2) / nBlockSize) * nBlockSize : nInLen * 3 + 1 >>> 2, aBytes = new Uint8Array(nOutLen);

            for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
                nMod4 = nInIdx & 3;
                nUint24 |= base.b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4;
                
                if (nMod4 === 3 || nInLen - nInIdx === 1) {
                    for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
                        aBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255;
                    }
                    nUint24 = 0;
                }
            }
            return aBytes;
        },

        /**
         * canvas转blob
         * @param {HTMLCanvasElement} canvas 
         * @param {string} type
         * @returns {Promise<Blob>}
         */
        canvas_to_blob: function(canvas, type="image/png") {
            return new Promise(
                resolve => canvas.toBlob(resolve, type, 1)
            );
        },

        /**
         * 合并blobs到压缩包,然后下载
         * @param {Iterable<Blob>} blobs 
         * @param {string} base_name 文件名通用部分,如 page-1.jpg 中的 page
         * @param {string} ext 扩展名,如 jpg
         * @param {string} zip_name 压缩包名称
         * @param {boolean} download 是否下载,可选,默认true,如果不下载则返回压缩包对象
         * @returns {"Promise<JSZip | null>"}
         */
        blobs_to_zip: async function(blobs, base_name, ext, zip_name, download=true) {
            const zip = new window.JSZip();
            // 归档
            for (const [i, blob] of this.enumerate(blobs)) {
                zip.file(`${base_name}-${i+1}.${ext}`, blob, { binary: true });
            }

            // 导出
            if (!download) {
                return zip;
            }

            const zip_blob = await zip.generateAsync({ type: "blob" });
            console.log(zip_blob);
            this.save(`${zip_name}.zip`, zip_blob);
            return null;
        },

        /**
         * 存储所有canvas图形为png到一个压缩包
         * @param {Iterable<HTMLCanvasElement>} canvases canvas元素列表
         * @param {string} title 文档标题
         */
        canvases_to_zip: async function(canvases, title) {
            // canvas元素转为png图像
            // 所有png合并为一个zip压缩包
            const tasks = [];
            for (let canvas of canvases) {
                tasks.push(this.canvas_to_blob(canvas));
            }
            const blobs = await this.gather(tasks);
            this.blobs_to_zip(blobs, "page", "png", title);
        },



        /**
         * 合并图像并导出PDF
         * @param {Iterable<HTMLCanvasElement | Uint8Array | HTMLImageElement>} imgs 图像元素列表
         * @param {string} title 文档标题
         * @param {number} width (可选)页面宽度 默认 0
         * @param {number} height (可选)页面高度 默认 0
         * @param {boolean} blob (可选)是否返回 blob 默认 false
         */
        imgs_to_pdf: async function(imgs, title, width = 0, height = 0, blob=false) {
            imgs = Array.from(imgs);
            if (imgs.length === 0) {
                this.raise("没有任何图像用于合并为PDF");
            }

            // 先获取第一个canvas用于判断竖向还是横向,以及得到页面长宽
            const first = imgs[0];

            // 如果没有手动指定canvas的长宽,则自动检测
            if (!width && !height) {
                // 如果是字节数组
                if (first instanceof Uint8Array) {
                    const cover = await createImageBitmap(
                        new Blob([first])
                    );
                    [width, height] = [cover.width, cover.height];

                    // 如果是画布或图像元素
                } else if (
                    first instanceof HTMLCanvasElement ||
                    first instanceof HTMLImageElement
                    ) {
                    if (first.width && parseInt(first.width) && parseInt(first.height)) {
                        [width, height] = [first.width, first.height];
                    } else {
                        const
                            width_str = first.style.width.replace(/(px)|(rem)|(em)/, ""),
                            height_str = first.style.height.replace(/(px)|(rem)|(em)/, "");
                        width = parseInt(width_str);
                        height = parseInt(height_str);
                    }
                } else {
                    // 其他未知类型
                    throw TypeError("不能处理的画布元素类型:" + this.classof(first));
                }
            }
            console.log(`canvas数据:宽: ${width}px,高: ${height}px`);
            
            // 如果文档第一页的宽比长更大,则landscape,否则portrait
            const orientation = width > height ? 'l' : 'p';
            // jsPDF的第三个参数为format,当自定义时,参数为数字数组。
            const pdf = new jspdf.jsPDF(orientation, 'px', [height, width]);

            const last = imgs.pop();
            const self = this;
            // 保存每一页文档到每一页pdf
            imgs.forEach((canvas, i) => {
                pdf.addImage(canvas, 'png', 0, 0, width, height);
                pdf.addPage();
                self?.update_popup(`PDF 已经绘制 ${i + 1} 页`);
            });
            // 添加尾页
            pdf.addImage(last, 'png', 0, 0, width, height);
            
            // 导出文件
            if (blob) {
                return pdf.output("blob");
            }
            pdf.save(`${title}.pdf`);
        },

        /**
         * imageBitMap转canvas
         * @param {ImageBitmap} bmp 
         * @returns {HTMLCanvasElement} canvas
         */
        bmp_to_canvas: function(bmp) {
            const canvas = document.createElement("canvas");
            canvas.height = bmp.height;
            canvas.width = bmp.width;
            
            const ctx = canvas.getContext("bitmaprenderer");
            ctx.transferFromImageBitmap(bmp);
            return canvas;
        },

        /**
         * 导出图片链接
         * @param {Iterable<string>} urls
         */
        save_urls: function(urls) {
            const _urls = Array
                .from(urls)
                .map((url) => {
                    const _url = url.trim();
                    if (url.startsWith("//"))
                        return "https:" + _url;
                    return _url;
                })
                .filter(url => url);

            this.save("urls.csv", _urls.join("\n"), "text/csv");
        },

        /**
         * 图片blobs合并并导出为单个PDF
         * @param {Array<Blob>} blobs 
         * @param {string} title (可选)文档名称, 不含后缀, 默认为"文档"
         * @param {boolean} filter (可选)是否过滤 type 不以 "image/" 开头的 blob; 默认为 true
         * @param {boolean} blob (可选)是否返回 blob,默认 false
         */
        img_blobs_to_pdf: async function(blobs, title="文档", filter=true, blob=false) {
            // 格式转换:img blob -> bmp
            let tasks = blobs;
            if (filter) {
                tasks = blobs.filter(
                    blob => blob.type.startsWith("image/")
                );
            }
            tasks = await this.gather(
                tasks.map(blob => blob.arrayBuffer())      
            );
            tasks = tasks.map(buffer => new Uint8Array(buffer));
            // 导出PDF
            return this.imgs_to_pdf(tasks, title, 0, 0, blob);
        },

        /**
         * 下载可以简单直接请求的图片,合并到 PDF 并导出
         * @param {Iterable<string>} urls 图片链接列表
         * @param {string} title 文档名称
         * @param {number} min_num 如果成功获取的图片数量 < min_num, 则等待 2 秒后重试; 默认 0 不重试
         * @param {boolean} clear 是否在请求完成后清理控制台输出,默认false
         * @param {boolean} blobs 是否返回二进制图片列表,默认 false(即直接导出PDF)
         */
        img_urls_to_pdf: async function(urls, title, min_num=0, clear=false, blobs=false) {
            // 强制转换为迭代器类型
            urls = urls[Symbol.iterator]();
            const first = urls.next().value;
            
            // 如果不符合同源策略,在打开新标签页
            if (!this.is_same_origin(first)) {
                console.info("URL 不符合同源策略;转为新标签页打开目标网站");
                this.open_in_new_tab((new URL(first)).origin);
                return;
            }

            let tasks, img_blobs, i = 3;
            // 根据请求成功数量判断是否循环
            do {
                i -= 1;
                // 发起请求
                tasks = [this.xhr_get_blob(first)];  // 初始化时加入第一个
                // 然后加入剩余的
                for (const [j, url] of this.enumerate(urls)) {
                    tasks.push(this.xhr_get_blob(url));
                    this.update_popup(`已经请求 ${j} 张图片`);
                }
                
                // 接收响应
                img_blobs = (await this.gather(tasks)).filter(
                    blob => blob.type.startsWith("image/")
                );

                if (clear) {
                    console.clear();
                }

                if (
                    min_num 
                    && img_blobs.length < min_num 
                    && i
                    ) {
                    // 下轮行动前冷却
                    console.log(`打盹 2 秒`);
                    await utils.sleep(2000);
                } else {
                    // 结束循环
                    break;
                }
            } while (true)

            if (blobs) return img_blobs;
            await this.img_blobs_to_pdf(img_blobs, title, false);
        },

        /**
         * 返回子串个数
         * @param {string} str 
         * @param {string} sub 
         */
        count_sub_str: function(str, sub) {
            return [...str.matchAll(sub)].length;
        },

        /**
         * 返回按钮区引用
         * @returns 
         */
        sec: function() {
            const sec = wk$(".wk-box .btns-sec")[0];
            if (!sec) throw new Error("wk 按钮区找不到");
            return sec;
        },

        _monkey: function() {
            const mky = wk$(".wk-box .wk-fold-btn")[0];
            if (!mky) throw new Error("wk 小猴子找不到");
            return mky;
        },

        /**
         * 折叠按钮区,返回是否转换了状态
         */
        fold_box: function() {
            const sec = this.sec();
            const mky = this._monkey();
            const display = getComputedStyle(sec).display;
            if (display !== "block") return false; 
            
            // 显示 -> 隐藏
            [sec, mky].forEach(
                elem => elem.classList.add("folded")
            );
            return true;
        },

        /**
         * 展开按钮区,返回是否转换了状态
         */
        unfold_box: function() {
            const sec = this.sec();
            const mky = this._monkey();
            const display = getComputedStyle(sec).display;
            if (display === "block") return false; 
            
            // 隐藏 -> 显示
            // 显示 -> 隐藏
            [sec, mky].forEach(
                elem => elem.classList.remove("folded")
            );
            return true;
        },

        /**
         * 运行基于按钮的、显示进度条的函数
         * @param {number} i 按钮序号
         * @param {Function} task 需要等待的耗时函数
         */
        run_with_prog: async function(i, task) {
            const btn = utils.btn(i);
            let new_btn;

            if (!wk$("#wk-popup")[0]) {
                this.add_popup();
            }

            this.fold_box();
            this.toID("wk-popup");

            new_btn = btn.cloneNode(true);
            btn.replaceWith(new_btn);
            this.onclick(
                () => utils.toID("wk-popup"), i, "显示进度"
            );

            try {
                await task();
            } catch(e) {
                console.error(e);
            }

            this.toID("");
            this.unfold_box();
            this.remove_popup();
            new_btn.replaceWith(btn);
        },

        /**
         * 创建5个按钮:展开文档、导出图片、导出PDF、未设定4、未设定5;除第1个外默认均为隐藏
         */
        create_btns: function() {
            // 添加样式
            document.head.insertAdjacentHTML("beforeend", style);
            // 添加按钮区
            document.body.insertAdjacentHTML("beforeend", box);

            // 绑定小猴子按钮回调
            const monkey = wk$(".wk-fold-btn")[0];
            // 隐藏【🙈】,展开【🐵】
            monkey.onclick = () => this.fold_box() || this.unfold_box();
        },

        /**
         * 添加弹窗到 body, 通过 utils.toID("wk-popup") 激发
         */
        add_popup: function() {
            document.body.insertAdjacentHTML("beforeend", popup);
        },

        /**
         * 设置弹窗正文
         * @param {string} text 
         */
        update_popup: function(text) {
            const body = wk$(".wk-popup-body")[0];
            if (!body) return;
            body.textContent = text;
        },

        /**
         * 移除弹窗
         */
        remove_popup: function() {
            this.remove(wk$(".wk-popup-container")[0]);
        },

        /**
         * 滚动页面到id位置的元素处
         * @param {string} id 
         */
        toID: function(id) {
            location.hash = `#${id}`;
        }
    };


    /**
     * ---------------------------------------------------------------------
     * 绑定使用 this 的函数到 utils,使其均成为绑定方法
     * ---------------------------------------------------------------------
     */

    /**
     * 确保特定外部脚本加载的装饰器
     * @param {string} global_obj_name 
     * @param {string} cdn_url 
     * @param {Function} func
     * @returns
     */
    function ensure_script_existed(global_obj_name, cdn_url, func) {
        async function inner(...args) {
            if (!window[global_obj_name]) {
                // 根据需要加载依赖
                await utils.load_web_script(cdn_url);
            }
            return func(...args);
        }
        // 存储参数定义
        base.superAssign(inner, func);
        return inner;
    }


    /**
     * 确保引用外部依赖的函数都在调用前加载了依赖
     */
    for (const prop of Object.keys(utils)) {
        // 跳过非函数
        if (
            !(typeof utils[prop] === "function")
            && !`${utils[prop]}`.startsWith("class")
            ) {  
            continue;
        }

        // 绑定this到utils
        if (/ this[.[][a-z_]/.test(`${utils[prop]}`)) {
            // 存储参数定义
            const doc = utils.help(utils[prop], false);
            // 绑死this,同时提供 __func__ 来取回原先的函数
            const fn = utils[prop];
            utils[prop] = utils[prop].bind(utils);
            utils[prop].__func__ = fn;
            // 重设参数定义
            utils[prop].__doc__ = doc;
        }

        // 设定 __doc__ 为访问器属性
        const doc_box = [
            utils.help(utils[prop], false)
        ];
        Object.defineProperty(utils[prop], "__doc__", {
            configurable: true,
            enumerable: true,
            get() { return doc_box.join("\n"); },
            set(new_doc) { doc_box.push(new_doc); },
        });

        // 为有外部依赖的函数做包装
        let obj, url;
        const name = prop.toLowerCase();

        if (name.includes("_to_zip")) {
            obj = "JSZip";
            url = "https://cdn.staticfile.org/jszip/3.7.1/jszip.min.js";

        } else if (name.includes("_to_pdf")) {
            obj = "jspdf";
            url = "https://cdn.staticfile.org/jspdf/2.5.1/jspdf.umd.min.js";

        } else {
            continue;
        }
        utils[prop] = ensure_script_existed(obj, url, utils[prop]);
    }


    /**
     * ---------------------------------------------------------------------
     * 为 utils 部分函数绑定更详细的说明
     * ---------------------------------------------------------------------
     */

    utils.b64_to_bytes.__doc__ = `
/**
 * b64编码字符串转Uint8Array
 * @param {string} sBase64 b64编码的字符串
 * @param {number} nBlockSize 字节数
 * @returns {Uint8Array} arr
 */
`;

    utils.blobs_to_zip.__doc__ = `
/**
 * 合并blobs到压缩包,然后下载
 * @param {Iterable<Blob>} blobs 
 * @param {string} base_name 文件名通用部分,如 image-1.jpg 中的 image
 * @param {string} ext 扩展名,如 jpg
 * @param {string} zip_name 压缩包名称
 */
`;

    utils.imgs_to_pdf.__doc__ = `
/**
 * 合并图像并导出PDF
 * @param {Iterable<HTMLCanvasElement | Uint8Array | HTMLImageElement>} imgs 图像元素列表
 * @param {string} title 文档标题
 * @param {number} width (可选)页面宽度 默认 0
 * @param {number} height (可选)页面高度 默认 0
 * @param {boolean} blob (可选)是否返回 blob 默认 false
 */
`;

    utils.img_urls_to_pdf.__doc__ = `
/**
 * 下载可以简单直接请求的图片,合并到 PDF 并导出
 * @param {Iterable<string>} urls 图片链接列表
 * @param {string} title 文档名称
 * @param {number} min_num 如果成功获取的图片数量 < min_num, 则等待 2 秒后重试; 默认 0 不重试
 * @param {boolean} clear 是否在请求完成后清理控制台输出,默认false
 */
`;

    utils.img_blobs_to_pdf.__doc__ = `
/**
 * 图片blobs合并并导出为单个PDF
 * @param {Array<Blob>} blobs 
 * @param {string} title (可选)文档名称, 不含后缀, 默认为"文档"
 * @param {boolean} filter (可选)是否过滤 type 不以 "image/" 开头的 blob; 默认为 true
 * @param {boolean} blob (可选)是否返回 blob
 */
`;


    /**
     * ---------------------------------------------------------------------
     * 绑定 utils 成员到 wk$,允许外部轻松调用
     * ---------------------------------------------------------------------
     */

    base.superAssign(wk$, utils);
    console.info("wk: `wk$` 已经挂载到全局");

    /**
     * 展开道客巴巴的文档
     */
    async function readAllDoc88() {
        // 获取“继续阅读”按钮
        let continue_btn = wk$("#continueButton")[0];
        // 如果存在“继续阅读”按钮
        if (continue_btn) {
            // 跳转到文末(等同于展开全文)
            let cur_page = wk$("#pageNumInput")[0];
            // 取得最大页码
            let page_max = cur_page.parentElement.textContent.replace(" / ", "");
            // 跳转到尾页
            utils.to_page(cur_page, page_max, "keypress");
            // 返回顶部
            await utils.sleep(1000);
            utils.to_page(cur_page, "1", "keypress");
        }
        // 文档展开后,显示按钮
        else {
            for (const i of utils.range(1, 6)) {
                utils.toggle_btn(i);
            }
        }
    }


    /**
     * 隐藏选择文字的弹窗
     */
    async function hideSelectPopup() {
        const
            elem = (await wk$$("#left-menu"))[0],
            hide = elem => elem.style.zIndex = -1;
        return utils.until_visible(elem).then(hide);
    }


    /**
     * 初始化任务
     */
    async function initService() {
        // 初始化
        console.log("正在执行初始化任务");

        // 1. 查找复制文字可能的api名称
        const prop = getCopyAPIValue();
        globalThis.doc88JS._apis = Object
            .getOwnPropertyNames(prop)
            .filter(name => {
                if (!name.startsWith("_")) {
                    return false;
                }
                if (prop[name] === "") {
                    return true;
                }
            });
        
        // 2. 隐藏选中文字的提示框
        await hideSelectPopup();
        // 3. 隐藏搜索框
        // hideSearchBox();
        // 4. 移除vip复制弹窗
        // hideCopyPopup();
    }


    /**
     * 取得 doc88JS.copy_api 所指向属性的值
     * @returns 
     */
    function getCopyAPIValue() {
        let aim = globalThis;
        for (let name of globalThis.doc88JS.copy_api) {
            aim = aim[name];
        }
        return aim;
    }


    /**
     * 返回选中的文字
     * @returns {string}
     */
    function getSelectedText() {
        // 首次复制文字,需要先找出api
        if (globalThis.doc88JS.copy_api.length === 3) {
            // 拼接出路径,得到属性
            let prop = getCopyAPIValue();  // 此时是属性,尚未取得值

            // 查询值
            for (let name of globalThis.doc88JS._apis) {
                let value = prop[name];
                // 值从空字符串变为非空字符串了,确认是目标api名称
                if (typeof value === 'string'
                    && value.length > 0
                    && !value.match(/\d/)  // 开头不能是数字,因为可能是 '1-179-195' 这种值
                    ) {
                    globalThis.doc88JS.copy_api.push(name);
                    break;
                }
            }
        }
        return getCopyAPIValue();
    }


    /**
     * 输出选中的文字到剪贴板和控制台,返回是否复制了文档
     * @returns {boolean} doc_is_copied
     */
    function copySelected() {
        // 尚未选中文字
        if (getComputedStyle(wk$("#left-menu")[0]).display === "none") {
            console.log("尚未选中文字");
            return false;
        }
        // 输出到控制台和剪贴板
        utils.copy_text(getSelectedText());
        return true;
    }


    /**
     * 捕获 ctrl + c 以复制文字
     * @param {KeyboardEvent} e 
     * @returns 
     */
    function onCtrlC(e) {
        // 判断是否为 ctrl + c
        if (!(e.code === "KeyC" && e.ctrlKey === true)) {
            return;
        }

        // 判断触发间隔
        let now = Date.now();
        // 距离上次小于0.5秒
        if (now - doc88JS.last_copy_time < 500 * 1) {
            doc88JS.last_copy_time = now;
            return;
        }
        // 大于1秒
        // 刷新最近一次触发时间
        doc88JS.last_copy_time = now;
        // 复制文字
        copySelected();
        // if (!copySelected()) return;
        
        // 停止传播
        e.stopImmediatePropagation();
        e.stopPropagation();
    }


    /**
     * 浏览并加载所有页面
     */
    async function walkThrough$2() {
        // 文档容器
        let container = wk$("#pageContainer")[0];
        container.style.display = "none";
        // 页码
        let page_num = wk$("#pageNumInput")[0];
        // 文末提示
        let tail = wk$("#readEndDiv > p")[0];
        let origin = tail.textContent;
        // 按钮
        wk$('.btns_section > [class*="btn-"]').forEach(
            elem => elem.style.display = "none"
        );

        // 逐页渲染
        let total = parseInt(Config.p_pagecount);
        try {
            for (let i = 1; i <= total; i++) {
                // 前往页码
                GotoPage(i);
                await utils.wait_until(async() => {
                    let page = wk$(`#page_${i}`)[0];
                    // page无法选中说明有弹窗
                    if (!page) {
                        // 关闭弹窗,等待,然后递归
                        wk$("#ym-window .DOC88Window_close")[0].click();
                        await utils.sleep(500);
                        walkThrough$2();
                        throw new Error("walkThrough 递归完成,终止函数");
                    }
                    // canvas尚未绘制时width=300
                    return page.width !== 300;
                });
                // 凸显页码
                utils.emphasize_text(page_num);
                tail.textContent = `请勿反复点击按钮,耐心等待页面渲染:${i}/${total}`;
            }
        } catch(e) {
            // 捕获退出信号,然后退出
            console.log(e);
            return;
        }

        // 恢复原本显示
        container.style.display = "";
        page_num.style = "";
        tail.textContent = origin;
        // 按钮
        wk$('.btns_section > [class*="btn-"]').forEach(
            elem => elem.style.display = "block"
        );
        wk$(".btns_section > .btn-1")[0].style.display = "none";
    }


    /**
     * 道客巴巴文档下载策略
     */
    async function doc88() {
        // 全局对象
        globalThis.doc88JS = {
            last_copy_time: 0,  // 上一次 ctrl + c 的时间戳(毫秒)
            copy_api: ["Core", "Annotation", "api"]
        };

        // 创建脚本启动按钮1、2
        utils.create_btns();

        // 绑定主函数
        let prepare = function() {
            // 获取canvas元素列表
            let node_list = wk$(".inner_page");
            // 获取文档标题
            let title;
            if (wk$(".doctopic h1")[0]) {
                title = wk$(".doctopic h1")[0].title;
            } else {
                title = "文档";
            }
            return [node_list, title];
        };

        // btn_1: 展开文档
        utils.onclick(readAllDoc88, 1);

        // // btn_2: 加载全部页面
        utils.onclick(walkThrough$2, 2, "加载所有页面");
        
        // btn_3: 导出PDF
        function imgsToPDF() {
            if (confirm("确定每页内容都加载完成了吗?")) {
                utils.run_with_prog(
                    3, () => utils.imgs_to_pdf(...prepare())
                );
            }
        }    utils.onclick(imgsToPDF, 3, "导出图片到PDF");

        // btn_4: 导出ZIP
        utils.onclick(() => {
            if (confirm("确定每页内容都加载完成了吗?")) {
                utils.canvases_to_zip(...prepare());
            }
        }, 4, "导出图片到ZIP");

        // btn_5: 复制选中文字
        utils.onclick(btn => {
            if (!copySelected()) {
                btn.textContent = "未选中文字";
            } else {
                btn.textContent = "复制成功!";
            }
        }, 5, "复制选中文字");

        // 为 ctrl + c 添加响应
        window.addEventListener("keydown", onCtrlC, true);
        // 执行一次初始化任务
        window.addEventListener(
            "mousedown", initService, { once: true, capture: true }
        );
    }

    function get_title$1() {
        return document.title.slice(0,-6);
    }


    function save_canvases(type) {
        return () => {
            if (!wk$(".hkswf-content2 canvas").length) {
                alert("当前页面不适用此按钮");
                return;
            }
        
            if (confirm("页面加载完毕了吗?")) {
                const title = get_title$1();
                const canvases = wk$(".hkswf-content2 canvas");
                let data_to;

                switch (type) {
                    case "pdf":
                        data_to = utils.imgs_to_pdf;
                        break;

                    case "zip":
                        data_to = utils.canvases_to_zip;
                        break;
                
                    default:
                        data_to = () => utils.raise(`未知 type: ${type}`);
                        break;
                }
                data_to(canvases, title);
            }
        }
    }


    function get_base_url() {
        // https://docimg1.docin.com/docinpic.jsp?file=2179420769&width=1000&sid=bZh4STs-f4NA88IA02INyapgA9Z5X3NN1sGo4WnpquIvk4CyflMk1Oxey1BsO1BG&pageno=2&pcimg=1
        return `https://docimg1.docin.com/docinpic.jsp?` +
            `file=` + location.pathname.match(/p-(\d+)[.]html/)[1] + 
            `&width=1000&sid=` + window.readerConfig.flash_param_hzq + 
            `&pcimg=1&pageno=`;
    }


    /**
     * 返回总页码
     * @returns {number}
     */
    function get_page_num() {
        return parseInt(
            wk$(".page_num")[0].textContent.slice(1)
        );
    }

    function init_save_imgs() {
        const iframe = document.createElement("iframe");
        iframe.src = "https://docimg1.docin.com/?wk=true";
        iframe.style.display = "none";
        
        let sock;

        /**
         * @param {MessageEvent} event 
         */
        function on_client_msg(event) {
            if (event.data.author !== "wk"
                || event.data.action !== "finish"
            ) return;
        
            sock.notListen(on_client_msg);
            iframe.remove();
            utils.toggle_btn(1);
            utils.toggle_btn(3);
        }
        
        /**
         * @param {string} type "pdf" | "zip"
         */
        return (type) => {
            return async function() {
                if (!wk$("[id*=img_] img").length) {
                    alert("当前页面不适用此按钮");
                    return;
                }
                
                utils.toggle_btn(1);
                utils.toggle_btn(3);

                document.body.append(iframe);
                await utils.sleep(500);
        
                sock = new utils.Socket(iframe.contentWindow);
                await sock.connect(false);
                sock.listen(on_client_msg);
                sock.talk({
                    author: "wk",
                    type,
                    title: get_title$1(),
                    base_url: get_base_url(),
                    max: get_page_num()
                });
            }
        }
    }


    const save_imgs = init_save_imgs();


    async function walk_through() {
        // 隐藏按钮
        utils.toggle_btn(5);
        // 隐藏文档页面
        wk$("#contentcontainer")[0].setAttribute("style", "visibility: hidden;");

        const total = get_page_num();
        const input = wk$("#page_cur")[0];
        
        for (let i = 1; i <= total; i++) {
            utils.to_page(input, i, "keydown");
            await utils.wait_until(
                () => {
                    const page = wk$(`#page_${i}`)[0];
                    const contents = wk$.call(page, `.canvas_loaded, img`);
                    return contents.length > 0;
                },
                5000
            );
        }

        // 显示文档页面
        wk$("#contentcontainer")[0].removeAttribute("style");
    }


    function main_page() {
        // 创建脚本启动按钮
        utils.create_btns();
        
        utils.onclick(
            save_imgs("pdf"), 1, "合并图片为PDF"
        );
        
        utils.onclick(
            save_canvases("pdf"), 2, "合并画布为PDF"
        );
        utils.toggle_btn(2);

        utils.onclick(
            save_imgs("zip"), 3, "打包图片到ZIP"
        );
        utils.toggle_btn(3);
        
        utils.onclick(
            save_canvases("zip"), 4, "打包画布到ZIP"
        );
        utils.toggle_btn(4);

        utils.onclick(
            walk_through, 5, "自动浏览页面"
        );
        utils.toggle_btn(5);
    }



    function init_background() {
        const sock = new utils.Socket(window.top);

        /**
         * @param {MessageEvent} event 
         */
        async function on_server_msg(event) {
            if (event.data.author !== "wk") return;
        
            const { title, base_url, max, type } = event.data;
            const urls = Array
                .from(utils.range(1, max + 1))
                .map(i => (base_url + i));
            
            const imgs = await utils.img_urls_to_pdf(
                urls, title, 0, false, true
            );
        
            switch (type) {
                case "pdf":
                    await utils.img_blobs_to_pdf(imgs, title);
                    break;
            
                case "zip":
                    const ext = imgs[0].type ? imgs[0].type.split("/")[1] : "png";
                    await utils.blobs_to_zip(
                        imgs, "page", ext, title
                    );
                    break;

                default:
                    utils.raise(`未知 type: ${type}`);
                    break;
            }

            sock.talk({
                author: "wk",
                action: "finish"
            });
            sock.notListen(on_server_msg);
        }
        
        return async function() {
            sock.listen(on_server_msg);
            await sock.connect(true);
        }
    }


    const background = init_background();


    /**
     * 豆丁文档下载策略
     */
    function docin() {
        const host = location.hostname;
        switch (host) {
            case "jz.docin.com":
            case "www.docin.com":
                main_page();
                break;

            case "docimg1.docin.com":
                background();
                break;
        
            default:
                console.log(`未知域名: ${host}`);
                break;
        }
    }

    function jumpToHost() {
        // https://swf.ishare.down.sina.com.cn/1DrH4Qt2cvKd.jpg?ssig=DUf5x%2BXnKU&Expires=1673867307&KID=sina,ishare&range={}-{}
        let url = wk$(".data-detail img, .data-detail embed")[0].src;
        if (!url) {
            alert("找不到图片元素");
            return;
        }

        let url_obj = new URL(url);
        let path = url_obj.pathname.slice(1);
        let query = url_obj.search.slice(1).split("&range")[0];
        let title = document.title.split(" - ")[0];
        let target = `${url_obj.protocol}//${url_obj.host}?path=${path}&fname=${title}&${query}`;
        // https://swf.ishare.down.sina.com.cn/
        globalThis.open(target, "hostage");
        // 然后在跳板页面发起对图片的请求
    }


    /**
     * 爱问文库下载跳转策略
     */
    function ishare() {
        // 创建按钮区
        utils.create_btns();

        // btn_1: 识别文档类型 -> 导出PDF
        utils.onclick(jumpToHost, 1, "到下载页面");
        // btn_2: 不支持爱问办公
        utils.onclick(() => null, 2, "不支持爱问办公");
        // utils.toggleBtnStatus(4);
    }

    /**
     * 返回包含对于数量svg元素的html元素
     * @param {string} data
     * @returns {HTMLDivElement} article
     */
    function _createDiv(data) {
        let num = utils.count_sub_str(data, data.slice(0, 10));
        let article = document.createElement("div");
        article.id = "article";
        article.innerHTML = `
        <style class="wk-settings">
            body {
                margin: 0px;
                width: 100%;
                background-color: rgb(95,99,104);
            }
            #article {
                width: 100%;
                display: flex;
                flex-direction: row;
                justify-content: space-around;
            }
            #root-box {
                display: flex;
                flex-direction: column;
                background-color: white;
                padding: 0 2em;
            }
            .gap {
                height: 50px;
                width: 100%;
                background-color: transparent;
            }
        </style>
        <div id="root-box">
        ${
            `<object class="svg-box"></object>
            <div class="gap"></div>`.repeat(num)
        }
    `;
        // 移除最后一个多出的gap
        Array.from(article.querySelectorAll(".gap")).at(-1).remove();
        return article;
    }


    function setGap(height) {
        let style = wk$(".wk-settings")[0].innerHTML;
        wk$(".wk-settings")[0].innerHTML = style.replace(
            /[.]gap.*?{.*?height:.+?;/s,
            `.gap { height: ${parseInt(height)}px;`    
        );
    }


    function setGapGUI() {
        let now = getComputedStyle(wk$(".gap")[0]).height;
        let new_h = prompt(`当前间距:${now}\n请输入新间距:`);
        if (new_h) {
            setGap(new_h);
        }
    }


    function getSVGtext(data) {
        let div = document.createElement("div"); 
        div.innerHTML = data;
        return div.textContent;
    }


    function toDisplayMode1() {
        let content = globalThis["ishareJS"].content_1;
        if (!content) {
            content = globalThis["ishareJS"].text
            .replace(/\n{2,}/g, "<hr>")
            .replace(/\n/g, "<br>")
            .replace(/\s/g, "&nbsp;")
            .replace(/([a-z])([A-Z])/g, "$1 $2");  // 英文简单分词

            globalThis["ishareJS"].content_1 = content;
        }

        wk$("#root-box")[0].innerHTML = content;
    }


    function toDisplayMode2() {
        let content = globalThis["ishareJS"].content_2;
        if (!content) {
            content = globalThis["ishareJS"].text
                .replace(/\n{2,}/g, "<hr>")
                .replace(/\n/g, "")
                .replace(/\s/g, "&nbsp;")
                .replace(/([a-z])([A-Z])/g, "$1 $2")
                .split("<hr>")
                .map(paragraph => `<p>${paragraph}</p>`)
                .join("");
            
                globalThis["ishareJS"].content_2 = content;
            wk$(".wk-settings")[0].innerHTML += `
            #root-box > p {
                text-indent: 2em;
                width: 40em;
                word-break: break-word;
            }
        `;
        }

        wk$("#root-box")[0].innerHTML = content;
    }


    function changeDisplayModeWrapper() {
        let flag = true;

        function inner() {
            if (flag) {
                toDisplayMode1();
            } else {
                toDisplayMode2();
            }
            flag = !flag;
        }
        return inner;
    }


    function handleSVGtext() {
        globalThis["ishareJS"].text = getSVGtext(
            globalThis["ishareJS"].data
        );

        let change = changeDisplayModeWrapper();
        utils.onclick(change, 4, "切换显示模式");

        utils.toggle_btn(2);
        utils.toggle_btn(3);
        utils.toggle_btn(4);
        change();
    }


    /**
     * 处理svg的url
     * @param {string} svg_url 
     */
    async function handleSVGurl(svg_url) {
        let resp = await fetch(svg_url);
        let data = await resp.text();
        globalThis["ishareJS"].data = data;

        let sep = data.slice(0, 10);
        let svg_texts = data
            .split(sep)
            .slice(1)
            .map(svg_text => sep + svg_text);

        console.log(`共 ${svg_texts.length} 张图片`);

        let article = _createDiv(data);
        let boxes = article.querySelectorAll(".svg-box");
        boxes.forEach((obj, i) => {
            let blob = new Blob([svg_texts[i]], {type: "image/svg+xml"});
            let url = URL.createObjectURL(blob);
            obj.data = url;
            URL.revokeObjectURL(blob);
        });

        let body = wk$("body")[0];
        body.innerHTML = "";
        body.appendChild(article);

        utils.create_btns();
        utils.onclick(utils.print_page, 1, "打印页面到PDF");
        utils.onclick(setGapGUI, 2, "重设页间距");
        utils.onclick(handleSVGtext, 3, "显示空白点我");

        utils.toggle_btn(2);
        utils.toggle_btn(3);
    }


    /**
     * 取得图片下载地址
     * @param {string} fname 
     * @param {string} path
     * @returns 
     */
    function getImgUrl(fname, path) {
        if (!fname) {
            throw new Error("URL Param `fname` does not exist.");
        } 
        return location.href
            .replace(/[?].+?&ssig/, "?ssig")
            .replace("?", path + "?");
    }


    /**
     * 下载整个图片包
     * @param {string} img_url
     * @returns 
     */
    async function getData(img_url) {   
        let resp = await fetch(img_url);
        // window.data = await resp.blob();
        // throw Error("stop");
        let buffer = await resp.arrayBuffer();
        return new Uint8Array(buffer);
    }


    /**
     * 分切图片包为若干图片
     * @param {Uint8Array} data 多张图片合集数据包
     * @returns {Array<Uint8Array>} 图片列表
     */
    function parseData(data) {
        // 判断图像类型/拿到文件头
        let head = data.slice(0, 8);
        return utils.split_files_by_head(data, head);
    }


    /**
     * 图像Uint8数组列表合并然后导出PDF
     * @param {string} fname
     * @param {Array<Uint8Array>} img_data_list 
     */
    async function imgDataArrsToPDF(fname, img_data_list) {
        return utils.imgs_to_pdf(
            img_data_list,
            fname
        );
    }


    /**
     * 
     * @param {string} fname 文件名
     * @param {Array<Uint8Array>} img_data_list 数据列表
     */
    async function saveAsZip(fname, img_data_list) {
        await utils.blobs_to_zip(
            img_data_list,
            "page",
            "png",
            fname
        );
    }


    /**
     * 取得图片集合体并切分,如果是 SVG 则对应处理
     * @returns {Array<Uint8Array>} imgs
     */
    async function getImgs() {
        let [fname, path] = [
            window.ishareJS.fname,
            window.ishareJS.path
        ];

        let img_url = getImgUrl(fname, path);

        // 处理svg
        if (path.includes(".svg")) {
            document.title = fname;
            await handleSVGurl(img_url);
            return;
        }
        // 处理常规图像
        let data = await getData(img_url);
        let img_data_list = parseData(data);
        console.log(`共 ${img_data_list.length} 张图片`);

        window.ishareJS.imgs = img_data_list;

        // 下载完成,可以导出
        utils.onclick(exportPDF$3, 2, "下载并导出PDF");
        utils.toggle_btn(1);
        utils.toggle_btn(2);
    }


    async function exportPDF$3() {
        let args = [
            window.ishareJS.fname,
            window.ishareJS.imgs
        ];

        try {
            await imgDataArrsToPDF(...args);
        } catch(e) {
            console.error(e);
            
            // 因 jsPDF 字符串拼接溢出导致的 Error
            if (`${e}`.includes("RangeError: Invalid string length")) {
                // 提示失败
                alert("图片合并为 PDF 时失败,请尝试下载图片压缩包");
                // 备选方案:导出图片压缩包
                utils.onclick(
                    () => saveAsZip(...args),
                    3,
                    "导出ZIP"
                );
                utils.toggle_btn(3);  // 显示导出ZIP按钮
                utils.toggle_btn(2);  // 隐藏导出PDF按钮
            } else {
                throw e;
            }
        }

    }


    function showHints() {
        wk$("h1")[0].textContent = "wk 温馨提示";
        wk$("p")[0].innerHTML = [
            "下载 270 页的 PPT (70 MB) 需要约 30 秒",
            "请耐心等待,无需反复点击按钮",
            "如果很久没反应,请加 QQ 群反馈问题"
        ].join("<br>");
        wk$("hr")[0].nextSibling.textContent = "403 Page Hostaged By Wenku Doc Downloader";
    }


    /**
     * 爱问文库下载策略
     */
    async function ishareData() {
        // 全局对象
        globalThis["ishareJS"] = {
            data: "",
            imgs: [],
            text: "",
            content_1: "",
            content_2: "",
            fname: utils.get_param("fname"),
            path: utils.get_param("path")
        };

        // 显示提示
        showHints();

        // 创建按钮区
        utils.create_btns();

        // btn_1: 识别文档类型,处理SVG或下载数据
        utils.onclick(getImgs, 1, "下载数据");
    }

    /**
     * 提供提示信息
     */
    function showTips$1() {
        const h2 = document.createElement("h2");
        h2.id = "wk-tips";
        document.body.append(h2);
    }


    /**
     * 更新文字到 h2 元素
     * @param {string} text 
     */
    function update(text) {
        wk$("#wk-tips")[0].textContent = text;
    }


    /**
     * 被动连接,取出数据,请求并分割图片,导出PDF
     */
    function mainTask() {
        const sock = new utils.Socket(opener);
        sock.listen(async e => {
            if (e.data.wk && e.data.action) {
                update("图片下载中,请耐心等待...");

                const url = e.data.img_url;
                const resp = await fetch(url);
                update("图片下载完成,正在解析...");

                const buffer = await resp.arrayBuffer();
                const whole_data = new Uint8Array(buffer);
                update("图片解析完成,正在合并...");
                
                await utils.imgs_to_pdf(
                    utils.split_files_by_head(whole_data),
                    e.data.title
                );
                update("图片合并完成,正在导出 PDF...");
            }
        });
        sock.connect(true);
    }


    /**
     * 爱问文库图片下载策略v2
     * @returns 
     */
    function ishareData2() {
        showTips$1();
        if (!(window.opener && window.opener.window)) {
            update("wk: 抱歉,页面出错了");
            return;
        }
        mainTask();
    }

    function getPageNum() {
        // ' / 6 ' -> ' 6 '
        return parseInt(
            wk$("span.counts")[0].textContent.split("/")[1]
        );
    }


    function jumpToHostage() {
        const
            // '/fileroot/2019-9/23/73598bfa-6b91-4cbe-a548-9996f46653a2/73598bfa-6b91-4cbe-a548-9996f46653a21.gif'
            url = new URL(wk$("#pageflash_1 > img")[0].src),
            num = getPageNum(),
            // '七年级上册地理期末试卷精编.doc-得力文库'
            fname = document.title.slice(0, -5),
            path = url.pathname,
            tail = "1.gif";
        
        if (!path.endsWith(tail)) {
            throw new Error(`url尾部不为【${tail}】!path:【${path}】`);
        }
        const base_path = path.slice(0, -5);
        open(`${url.protocol}//${url.host}/?num=${num}&lmt=${lmt}&fname=${fname}&path=${base_path}`);
    }


    function deliwenku() {
        utils.create_btns();
        utils.onclick(jumpToHostage, 1, "到下载页面");
    }

    function showTips() {
        const body = `
        <style>
            h1 { color: black; } 
            #main {
                margin: 1vw 5%;
                border-radius: 10%;
            }
            p { font-size: large; }
            .info {
                color: rgb(230,214,110);
                background: rgb(39,40,34);
                text-align: right;
                font-size: medium;
                padding: 1vw;
                border-radius: 4px;
            }
        </style>
        <div id="main">
            <h1>wk: 跳板页面</h1>
            <p>有时候点一次下载等半天没反应,就再试一次</p>
            <p>如果试了 2 次还不行加 QQ 群反馈吧...</p>
            <p>导出的 PDF 如果页面数量少于应有的,那么意味着免费页数就这么多,我也爱莫能助</p>
            <p>短时间连续使用导出按钮会导致 IP 被封禁</p>
            <hr>
            <div class="info">
                文档名称:${deliJS.fname}<br>
                原始文档页数:${deliJS.num}<br>
                最大免费页数:${deliJS.lmt}<br>
            </div>
        </div>`;
        document.title = utils.get_param("fname");    document.body.innerHTML = body;
    }


    /**
     * url生成器
     * @param {string} base_url 
     * @param {number} num 
     */
    function* genURLs(base_url, num) {
        for (let i=1; i<=num; i++) {
            yield `${base_url}${i}.gif`;
        }
    }


    function genBaseURL(path) {
        return `${location.protocol}//${location.host}${path}`;
    }


    function parseParamsToDeliJS() {
        const
            base_url = genBaseURL(utils.get_param("path")),
            fname = utils.get_param("fname"),
            num = parseInt(utils.get_param("num"));

        let lmt = parseInt(utils.get_param("lmt"));
        lmt = lmt > 3 ? lmt : 20;
        lmt = lmt > num ? num : lmt;

        window.deliJS = {
            base_url,
            num,
            fname,
            lmt
        };
    }


    async function exportPDF$2() {
        utils.toggle_btn(1);
        await utils.run_with_prog(
            1, () => utils.img_urls_to_pdf(
                genURLs(deliJS.base_url, deliJS.num),
                deliJS.fname,
                deliJS.lmt,
                true  // 请求完成后清理控制台
            )
        );
        utils.toggle_btn(1);
    }


    /**
     * 得力文库跳板页面下载策略
     */
    async function deliFile() {
        // 从URL解析文档参数
        parseParamsToDeliJS();
        // 显示提示
        showTips();

        // 创建按钮区
        utils.create_btns();
        // btn_1: 导出PDF
        utils.onclick(exportPDF$2, 1, "导出PDF");
    }

    function readAll360Doc() {
        // 展开文档
        document.querySelector(".article_showall a").click();
        // 隐藏按钮
        utils.toggle_btn(1);
        // 显示按钮
        utils.toggle_btn(2);
        utils.toggle_btn(3);
        utils.toggle_btn(4);
    }


    function saveText_360Doc() {
        // 捕获图片链接
        let images = wk$("#artContent img");
        let content = [];

        for (let i = 0; i < images.length; i++) {
            let src = images[i].src;
            content.push(`图${i+1},链接:${src}`);
        }
        // 捕获文本
        let text = wk$("#artContent")[0].textContent;
        content.push(text);

        // 保存纯文本文档
        let title = wk$("#titiletext")[0].textContent;
        utils.save(`${title}.txt`, content.join("\n"));
    }


    /**
     * 使文档在页面上居中
     * @param {string} selector 文档容器的css选择器
     * @param {string} default_offset 文档部分向右偏移的百分比(0-59)
     * @returns 偏移值是否合法
     */
    function centre(selector, default_offset) {
        const elem = wk$(selector)[0];
        const offset = prompt("请输入偏移百分位:", default_offset);
        
        // 如果输入的数字不在 0-59 内,提醒用户重新设置
        if (offset.length === 1 && offset.search(/[0-9]/) !== -1) {
            elem.style.marginLeft = offset + "%";
            return true;
        }

        if (offset.length === 2 && offset.search(/[1-5][0-9]/) !== -1) {
            elem.style.marginLeft = offset + "%";
            return true;
        }

        alert("请输入一个正整数,范围在0至59之间,用来使文档居中");
        return false;
    }


    function printPage360Doc() {
        if (!confirm("确定每页内容都加载完成了吗?")) {
            return;
        }
        // # 清理并打印360doc的文档页
        // ## 移除页面上无关的元素
        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";
        let elem_list = wk$(selector);
        let under_doc_1, under_doc_2;
        try {
            under_doc_1 = wk$("#bgchange p.clearboth")[0].nextElementSibling;
            under_doc_2 = wk$("#bgchange")[0].nextElementSibling.nextElementSibling;
        } catch (e) {}
        // 执行移除
        for (let elem of elem_list) {
            utils.remove(elem);
        }
        utils.remove(under_doc_1);
        utils.remove(under_doc_2);
        // 执行隐藏
        wk$("a[title]")[0].style.display = "none";

        // 使文档居中
        alert("建议使用:\n偏移量: 20\n缩放: 默认\n");
        if (!centre(".a_left", "20")) {
            return; // 如果输入非法,终止函数调用
        }
        // 隐藏按钮,然后打印页面
        utils.print_page();
    }


    /**
     * 阻止监听器生效
     * @param {Event} e 
     */
    function stopSpread(e) {
        e.stopImmediatePropagation();
        e.stopPropagation();
    }


    /**
     * 阻止捕获事件
     */
    function stopCapturing() {
        ["click", "mouseup"].forEach(
            type => {
                document.body.addEventListener(type, stopSpread, true);
                document["on" + type] = undefined;
            }
        );
        
        ["keypress", "keydown"].forEach(
            type => {
                window.addEventListener(type, stopSpread, true);
                window["on" + type] = undefined;
            }
        );
    }


    /**
     * 重置图像链接和最大宽度
     * @param {Document} doc
     */
    function resetImg(doc=document) {
        wk$.call(doc, "img").forEach(
            elem => {
                elem.style.maxWidth = "100%";
                for (let attr of elem.attributes) {
                    if (attr.name.endsWith("-src")) {
                        elem.setAttribute("src", attr.value);
                        break;
                    }
                }
            }
        );
    }


    /**
     * 仅保留全屏文档
     */
    function getFullScreen() {
        FullScreenObj.init();
        wk$("#artContent > p:nth-child(3)")[0]?.remove();
        let data = wk$("#artfullscreen__box_scr > table")[0].outerHTML;
        window.doc360JS = { data };
        let html_str = `
        <html><head></head><body style="display: flex; flex-direction: row; justify-content: space-around">
            ${data}
        </body><html>
    `;
        wk$("html")[0].replaceWith(wk$("html")[0].cloneNode());
        wk$("html")[0].innerHTML = html_str;
        resetImg();
    }


    function cleanPage() {
        getFullScreen();
        stopCapturing();
    }


    /**
     * 360doc个人图书馆下载策略
     */
    function doc360() {
        // 创建按钮区
        utils.create_btns();
        // btn_1: 展开文档
        utils.onclick(readAll360Doc, 1);
        // btn_2: 导出纯文本
        utils.onclick(saveText_360Doc, 2, "导出纯文本");
        // btn_3: 打印页面到PDF
        utils.onclick(printPage360Doc, 3, "打印页面到PDF");
        // btn_3: 清理页面
        utils.onclick(cleanPage, 4, "清理页面(推荐)");
    }

    async function getPDF() {
        if (!window.DEFAULT_URL) {
            alert("当前文档无法解析,请加 QQ 群反馈");
            return;
        }
        let title = document.title.split(" - ")[0] + ".pdf";
        let blob = await utils.xhr_get_blob(DEFAULT_URL);
        utils.save(title, blob);
    }


    function mbalib() {
        utils.create_btns();
        utils.onclick(getPDF, 1, "下载PDF");
    }

    /**
     * 判断是否进入预览模式
     * @returns Boolean
     */
    function isInPreview() {
        let p_elem = wk$("#preview_tips")[0];
        if (p_elem && p_elem.style && p_elem.style.display === "none") {
            return true;
        }
        return false;
    }


    /**
     * 确保进入预览模式
     */
    async function ensureInPreview() {
        while (!isInPreview()) {
            // 如果没有进入预览,则先进入
            if (typeof window.preview !== "function") {
                alert("脚本失效,请加 QQ 群反馈");
                throw new Error("preview 全局函数不存在");
            }

            await utils.sleep(500);
            preview();
        }
    }


    /**
     * 前往页码
     * @param {number} page_num 
     */
    function toPage(page_num) {
        // 先尝试官方接口,不行再用模拟的
        try {
            Viewer._GotoPage(page_num);
        } catch(e) {
            console.error(e);
            utils.to_page(
                wk$("#pageNumInput")[0],
                page_num,
                "keydown"
            );
        }
    }


    /**
     * 展开全文预览,当展开完成后再次调用时,返回true
     * @returns 
     */
    async function walkThrough$1() {
        // 隐藏页面
        wk$("#pageContainer")[0].style.display = "none";

        // 逐页加载
        let lmt = window.dugenJS.lmt;
        for (let i of utils.range(1, lmt + 1)) {
            toPage(i);
            await utils.wait_until(
                () => wk$(`#outer_page_${i}`)[0].style.width.endsWith("px")
            );
        }

        // 恢复显示
        wk$("#pageContainer")[0].style.display = "";
        console.log(`共 ${lmt} 页加载完毕`);
    }


    /**
     * 返回当前未加载页面的页码
     * @returns not_loaded
     */
    function getNotloadedPages() {
        // 已经取得的页码
        let pages = document.querySelectorAll("[id*=pageflash_]");
        let loaded = new Set();
        pages.forEach((page) => {
            let id = page.id.split("_")[1];
            id = parseInt(id);
            loaded.add(id);
        });
        // 未取得的页码
        let not_loaded = [];
        for (let i of utils.range(1, window.dugenJS.lmt + 1)) {
            if (!loaded.has(i)) {
                not_loaded.push(i);
            }
        }
        return not_loaded;
    }


    /**
     * 取得全部文档页面的链接,返回urls;如果有页面未加载,则返回null
     * @returns
     */
    function getImgUrls() {
        let pages = wk$("[id*=pageflash_]");
        // 尚未浏览完全部页面,返回false
        if (pages.length < window.dugenJS.lmt) {
            let hints = [
                "尚未加载完全部页面",
                "以下页面需要浏览并加载:",
                getNotloadedPages().join(",")
            ];
            alert(hints.join("\n"));
            return [false, []];
        }
        // 浏览完全部页面,返回urls
        return [true, pages.map(page => page.querySelector("img").src)];
    }


    function exportImgUrls() {
        let [ok, urls] = getImgUrls();
        if (!ok) {
            return;
        }
        utils.save("urls.csv", urls.join("\n"));
    }


    function exportPDF$1() {
        let [ok, urls] = getImgUrls();
        if (!ok) {
            return;
        }
        let title = document.title.split("-")[0];
        return utils.run_with_prog(
            3, () => utils.img_urls_to_pdf(urls, title)
        );
    }


    /**
     * dugen文档下载策略
     */
    async function dugen() {
        await ensureInPreview();
        // 全局对象
        window.dugenJS = {
            lmt: window.lmt ? window.lmt : 20
        };

        // 创建按钮区
        utils.create_btns();

        // 绑定监听器
        // 按钮1:展开文档
        utils.onclick(walkThrough$1, 1, "加载可预览页面");
        // 按钮2:导出图片链接
        utils.onclick(exportImgUrls, 2, "导出图片链接");
        utils.toggle_btn(2);
        // 按钮3:导出PDF
        utils.onclick(exportPDF$1, 3, "导出PDF");
        utils.toggle_btn(3);
    }

    // 域名级全局常量
    const img_tasks = [];


    /**
     * 取得文档类型
     * @returns {String} 文档类型str
     */
    function getDocType() {
        const
            // ["icon", "icon-format", "icon-format-doc"]
            elem = wk$(".title .icon.icon-format")[0],
            // "icon-format-doc"
            cls = elem.classList[2];    
        return cls.split("-")[2];
    }


    /**
     * 判断文档类型是否为type_list其中之一
     * @returns 是否为type
     */
    function isTypeof(type_list) {
        const type = getDocType();
        if (type_list.includes(type)) {
            return true;
        }
        return false;
    }


    /**
     * 判断文档类型是否为PPT
     * @returns 是否为PPT
     */
    function is_ppt() {
        return isTypeof(["ppt", "pptx"]);
    }


    /**
     * 判断文档类型是否为Excel
     * @returns 是否为Excel
     */
    function is_excel() {
        return isTypeof(["xls", "xlsm", "xlsx"]);
    }


    /**
     * 取得未加载页面的页码
     * @returns {Array} not_loaded 未加载页码列表
     */
    function getNotLoaded() {
        const loaded = wk$("[data-id] img[src]").map(
            img => parseInt(
                img.closest("[data-id]").getAttribute("data-id")
            )
        );
        return Array.from(
            utils.diff(
                utils.range(1, window.book118JS.page_counts + 1),
                loaded
            )
        );
    }


    /**
     * 取得全部文档页的url
     * @returns [<是否全部加载>, <urls列表>, <未加载页码列表>]
     */
    function getUrls() {
        const urls = wk$("[data-id] img[src]").map(
            img => img.src
        );
        // 如果所有页面加载完毕
        if (urls.length === book118JS.page_counts) {
            return [true, urls, []];
        }
        // 否则收集未加载页面的url
        return [false, urls, getNotLoaded()];
    }


    /**
     * 展开全文
     */
    async function walkThrough() {
        // 遍历期间隐藏按钮区
        utils.toggle_box();

        // 取得总页码
        // preview.getPage()
        // {current: 10, actual: 38, preview: 38, remain: 14}
        const { preview: all } = preview.getPage();
        for (let i = 1; i <= all; i++) {
            // 逐页加载
            preview.jump(i);
            await utils.wait_until(
                () => wk$(`[data-id="${i}"] img`)[0].src, 1000
            );
        }
        console.log("遍历完成");
        utils.toggle_box();
    }


    /**
     * btn_2: 导出图片链接
     */
    function wantUrls() {
        let [flag, urls, escaped] = getUrls();
        // 页面都加载完毕,下载urls
        if (!flag) {
            // 没有加载完,提示出未加载好的页码
            const hint = [
                "仍有页面没有加载",
                "请浏览并加载如下页面",
                "是否继续导出图片链接?",
                "[" + escaped.join(",") + "]"
            ].join("\n");
            // 终止导出
            if (!confirm(hint)) {
                return
            }
        }
        utils.save("urls.csv", urls.join("\n"));
    }


    /**
     * 打开PPT预览页面
     */
    async function open_iframe() {
        wk$(".front a")[0].click();
        const iframes = await wk$$("iframe.preview-iframe");
        window.open(iframes[0].src);
    }


    /**
     * 取得最大页码
     * @returns {number} 最大页码
     */
    function getPageCounts$1() {
        return window?.preview?.getPage()?.preview || NaN;
    }


    /**
     * 原创力文档(非PPT或Excel)下载策略
     */
    async function common_doc() {
        await utils.wait_until(
            () => !!wk$(".counts")[0]
        );

        // 创建全局对象
        window.book118JS = {
            doc_type: getDocType(),
            page_counts: getPageCounts$1()
        };

        // 处理非PPT文档
        // 创建按钮组
        utils.create_btns();
        // 绑定监听器到按钮
        // 按钮1:加载全文
        utils.onclick(walkThrough, 1, "加载全文");
        // 按钮2:导出图片链接
        utils.onclick(wantUrls, 2, "导出图片链接");
        utils.toggle_btn(2);
    }


    /**
     * @returns {string}
     */
    function table_to_tsv() {
        return wk$("table").map(table => {
            // 剔除空表和行号表
            const len = table.rows.length;
            if (len > 1000 || len === 1) {
                return "";
            }

            // 遍历行
            return [...table.rows].map(row => {
                // 遍历列(单元格)
                return [...row.cells].map(cell => {
                    // 判断单元格是否存储图片
                    const img = cell.querySelector("img");
                    if (img) {
                        // 如果是图片,保存图片链接
                        return img.src;
                    }
                    
                    // 否则保存单元格文本
                    return cell
                        .textContent
                        .trim()
                        .replace(/\n/g, "  ")
                        .replace(/\t/g, "    ");
                }).join("\t");
            }).join("\n").trim();
        }).join("\n\n---\n\n");
    }


    /**
     * 下载当前表格内容,保存为csv(utf-8编码)
     */
    function wantEXCEL() {
        const tsv = table_to_tsv();
        const bytes = utils.encode_to_gbk(tsv);
        const fname = "原创力表格.tsv";
        utils.save(fname, bytes);
    }


    /**
     * 在Excel预览页面给出操作提示
     */
    function help$1() {
        const hint = [
            "【导出表格到TSV】只能导出当前 sheet",
            "如果有多张 sheet 请在每个 sheet 上用按钮分别导出 TSV",
            "TSV 文件请用记事本或 Excel 打开",
            "TSV 不能存储图片,所以用图片链接代替",
            "或使用此脚本复制表格到剪贴板:",
            "https://greasyfork.org/zh-CN/scripts/469550",
        ];
        alert(hint.join("\n"));
    }


    /**
     * 原创力文档(EXCEL)下载策略
     */
    function excel() {
        // 创建按钮区
        utils.create_btns();
        // 绑定监听器到按钮
        utils.onclick(wantEXCEL, 1, "导出表格到TSV");
        utils.onclick(help$1, 2, "使用说明");
        // 显示按钮
        utils.toggle_btn(2);
    }


    /**
     * ------------------------------ PPT 策略 ---------------------------------
     */


    /**
     * 返回当前页码
     * @returns {number}
     */
    function cur_page_num() {
        return parseInt(
            wk$("#PageIndex")[0].textContent
        );
    }


    function add_page() {
        const view = wk$("#view")[0];
        view.setAttribute("style", "");

        const i = cur_page_num() - 1;
        const cur_view = wk$(`#view${i}`)[0];

        img_tasks.push(
            html2canvas(cur_view)
        );
        utils.btn(1).textContent = `截图: ${img_tasks.length}`;
    }


    function reset_tasks() {
        img_tasks.splice(0);
        utils.btn(1).textContent = `截图: 0`;
    }


    function canvas_to_blob(canvas) {
        return utils.canvas_to_blob(canvas);
    }


    async function export_imgs_as_pdf() {
        alert("正在合并截图,请耐心等待");
        utils.toggle_btn(3);

        try {
            const imgs = await utils.gather(img_tasks);
            const blobs = await utils.gather(
                imgs.map(canvas_to_blob)
            );

            if (!blobs.length) {
                alert("你尚未截取任何页面!");
            } else {
                await utils.img_blobs_to_pdf(blobs, "原创力幻灯片");
            }
        } catch(err) {
            console.error(err);
        }
        
        utils.toggle_btn(3);
    }



    function ppt() {
        utils.create_btns();

        const btn1 = utils.btn(1);
        btn1.onclick = add_page;
        btn1.textContent = "截图当前页面";

        utils.onclick(reset_tasks, 2, "清空截图");
        utils.onclick(export_imgs_as_pdf, 3, "合并为PDF");

        utils.toggle_btn(2);
        utils.toggle_btn(3);
    }


    /**
     * 原创力文档下载策略
     */
    function book118() {
        const host = window.location.hostname;

        if (host === 'max.book118.com') {
            if (is_excel()) {
                utils.create_btns();
                utils.onclick(open_iframe, 1, "访问EXCEL");
            } else if (is_ppt()) {
                utils.create_btns();
                utils.onclick(open_iframe, 1, "访问PPT");
            } else {
                common_doc();
            }
        
        } else if (wk$("#ppt")[0]) {
            if (window.top !== window) return;
            ppt();
        
        } else if (wk$(`[src*="excel.min.js"]`)[0]) {
            excel();
        
        } else {
            console.log(`wk: Unknown host: ${host}`);
        }
    }

    // test url: https://openstd.samr.gov.cn/bzgk/gb/newGbInfo?hcno=E86BBCE32DA8E67F3DA04ED98F2465DB


    /**
     * 绘制0x0的bmp, 作为请求失败时返回的page
     * @returns {Promise<ImageBitmap>} blank_page
     */
    async function blankBMP() {
        let canvas = document.createElement("canvas");
        [canvas.width, canvas.height] = [0, 0];
        return createImageBitmap(canvas);
    }


    /**
     * resp导出bmp
     * @param {string} page_url 
     * @param {Promise<Response> | ImageBitmap} pms_or_bmp 
     * @returns {Promise<ImageBitmap>} page
     */
    async function respToPage(page_url, pms_or_bmp) {
        let center = globalThis.gb688JS;
        // 此时是bmp
        if (pms_or_bmp instanceof ImageBitmap) {
            return pms_or_bmp;
        }

        // 第一次下载, 且无人处理
        if (!center.pages_status.get(page_url)) {
            // 处理中, 设为占用
            center.pages_status.set(page_url, 1);

            // 处理
            let resp;
            try {
                resp = await pms_or_bmp;
            } catch(err) {
                console.log("下载页面失败");
                console.error(err);
                return blankBMP();
            }

            let page_blob = await resp.blob();
            let page = await createImageBitmap(page_blob);
            center.pages.set(page_url, page);
            
            // 处理结束, 设为释放
            center.pages_status.set(page_url, 0);
            return page;
        }

        // 有人正在下载且出于处理中
        while (center.pages_status.get(page_url)) {
            await utils.sleep(500);
        }
        return center.pages.get(page_url);
    }


    /**
     * 获得PNG页面
     * @param {string} page_url 
     * @returns {Promise<ImageBitmap>} bmp
     */
    async function getPage(page_url) {
        // 如果下载过, 直接返回缓存
        let pages = globalThis.gb688JS.pages;
        if (pages.has(page_url)) {
            return respToPage(page_url, pages.get(page_url));
        }

        // 如果从未下载过, 就下载
        let resp = fetch(page_url, {
            "headers": {
                "accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
                "accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
                "proxy-connection": "keep-alive"
            },
            "referrer": location.href,
            "referrerPolicy": "strict-origin-when-cross-origin",
            "body": null,
            "method": "GET",
            "mode": "cors",
            "credentials": "include"
        });
        pages.set(page_url, resp);
        return respToPage(page_url, resp);
    }


    /**
     * 返回文档页div的裁切和粘贴位置信息: [[cut_x, cut_y, paste_x%, paset_y%],...]
     * @param {HTMLDivElement} page_div 文档页元素
     * @returns {Array<Array<number>>} positions
     */
    function getPostions(page_div) {
        let positions = [];

        Array.from(page_div.children).forEach(span => {
            // 'pdfImg-3-8' -> {left: 30%; top: 80%;}
            let paste_pos = span.className.split("-").slice(1).map(
                v => parseInt(v) / 10
            );
            // '-600px 0px' -> [600, 0]
            let cut_pos = span.style.backgroundPosition.split(" ").map(
                v => Math.abs(parseInt(v))
            );
            positions.push([...cut_pos, ...paste_pos]);
        });
        return positions;
    }


    /**
     * 取得文档页的图像url
     * @param {HTMLDivElement} page_div 
     * @returns {string} url
     */
    function getPageURL(page_div) {
        // 拿到目标图像url
        let path = location.pathname.split("/").slice(0, -1).join("/");
        let prefix = location.origin + path + "/";
        let url = page_div.getAttribute("bg");
        if (!url) {
            // 'url("viewGbImg?fileName=VS72l67k0jw5g3j0vErP8DTsnWvk5QsqnNLLxaEtX%2FM%3D")'
            url = page_div.children[0].style.backgroundImage.split('"')[1];
        }
        return prefix + url;
    }


    /**
     * 下载目标图像并拆解重绘, 返回canvas
     * @param {number} i 第 i 页 (从0开始)
     * @param {HTMLDivElement} page_div
     * @returns {Promise<Array>} [页码, Canvas]
     */
    async function getAndDrawPage(i, page_div) {
        // 拿到目标图像
        let url = getPageURL(page_div);
        let page = await getPage(url);

        // 绘制空白A4纸背景
        const [page_w, page_h] = [
            parseInt(page_div.style.width),
            parseInt(page_div.style.height)
        ];
        let bg = document.createElement("canvas");
        bg.width = page_w;  // 注意canvas作为取景框的大小
        bg.height = page_h;  // 如果不设置等于一个很小的取景框(300x300)
        
        let bg_ctx = bg.getContext("2d");
        bg_ctx.fillStyle = "white";
        bg_ctx.fillRect(0, 0, page_w, page_h);

        // 逐个区块剪切取出并粘贴
        const [part_w, part_h] = [page_w / 10, page_h / 10];
        getPostions(page_div).forEach(pos => {
            bg_ctx.drawImage(
                page,  // image source
                pos[0],  // source x
                pos[1],  // source y
                part_w,  // source width
                part_h,  // source height
                pos[2] * page_w,  // destination x = left: x%
                pos[3] * page_h,  // destination y = top: y%
                part_w,  // destination width
                part_h  // destination height
            );
        });
        // });
        return [i, bg];
    }


    /**
     * 页面批量请求、裁剪重绘, 合成PDF并下载
     */
    async function turnPagesToPDF() {
        // 渲染每页
        const tasks = wk$("#viewer .page").map(
            (page_div, i) => getAndDrawPage(i, page_div)
        );
        
        // 等待每页渲染完成后,排序
        const results = await utils.gather(tasks);
        results.sort((prev, next) => prev[0] - next[0]);
        
        // 合并为PDF并导出
        return utils.imgs_to_pdf(
            results.map(item => item[1]),
            // '在线预览|GB 14023-2022'
            document.title.split("|")[1]
        );
    }


    /**
     * 提示预估下载耗时,然后下载
     */
    function hintThenDownload$1() {
        // '/93'
        let page_num = parseInt(wk$("#numPages")[0].textContent.slice(1));
        let estimate = Math.ceil(page_num / 3);
        alert(`页数: ${page_num},预计花费: ${estimate}秒;如遇网络异常可能更久\n请勿反复点击按钮;如果无法导出请 QQ 群反馈`);
        turnPagesToPDF();
    }


    /**
     * gb688文档下载策略
     */
    async function gb688() {
        // 创建全局对象
        globalThis.gb688JS = {
            pages: new Map(),  // {url: bmp}
            pages_status: new Map()  // {url: 0或1} 0释放, 1占用
        };

        // 创建按钮区
        utils.create_btns();
        // 绑定监听器
        // 按钮1:导出PDF
        utils.onclick(hintThenDownload$1, 1, "导出PDF");
    }

    function getPageCounts() {
        // " / 39"
        const counts_str = wk$(".counts")[0].textContent.split("/")[1];
        const counts = parseInt(counts_str);
        return counts > 20 ? 20 : counts;
    }


    /**
     * 返回图片基础路径
     * @returns {string} base_url
     */
    function getImgBaseURL() {
        return wk$("#dp")[0].value;
    }


    function* genImgURLs$1() {
        let counts = getPageCounts();
        let base_url = getImgBaseURL();
        for (let i = 1; i <= counts; i++) {
            yield base_url + `${i}.gif`;
        }
    }


    /**
     * 下载图片,转为canvas,合并为PDF并下载
     * @returns {Promise<void>}
     */
    function fetchThenExportPDF() {
        // db2092-2014-河北特种设备使用安全管理规范_安全文库网safewk.com
        let title = document.title.split("_")[0];
        return utils.img_urls_to_pdf(genImgURLs$1(), title);
    }


    /**
     * 提示预估下载耗时,然后下载
     */
    function hintThenDownload() {
        let hint = [
            "只能导出可预览的页面(最多20页)",
            "请勿短时间反复点击按钮,导出用时大约不到 10 秒",
            "点完后很久没动静请至 QQ 群反馈"
        ];
        alert(hint.join("\n"));
        return utils.run_with_prog(
            1, fetchThenExportPDF
        );
    }


    /**
     * safewk文档下载策略
     */
    async function safewk() {
        // 创建按钮区
        utils.create_btns();
        // 绑定监听器
        // 按钮1:导出PDF
        utils.onclick(
            hintThenDownload, 1, "导出PDF"
        );
    }

    /**
     * 跳转到页码
     * @param {string | number} num 
     */
    function _to_page(num) {
        if (window.WebPreview
            && WebPreview.Page
            && WebPreview.Page.jump
        ) {
            WebPreview.Page.jump(parseInt(num));
        } else {
            console.error("window.WebPreview.Page.jump doesn't exist");
        }
    }


    /**
     * 跳转页码GUI版
     */
    function to_page() {
        let num = prompt("请输入要跳转的页码")?.trim();
        if (/^[0-9]+$/.test(num)) {
            _to_page(num);
        } else {
            console.log(`输入值 [${num}] 不是合法整数`);
        }
    }


    function capture_urls() {
        if (!confirm(
            "只能导出已经预览页面的链接,是否继续?"
        )) return;

        let imgs = wk$("[data-id] img");
        if (imgs.length === 0) {
            imgs = wk$("img[data-page]");
        }
        console.log(imgs);

        const urls = imgs.map(img => {
            const src = img.dataset.src || img.src;
            if (!src) return;
            return src.startsWith("//") ? "https:" + src : src
        });
        
        const lacked = [];
        const existed = urls.filter((url, i) => {
            if (url) return true;
            lacked.push(i + 1);
        });

        utils.save_urls(existed);
        alert(
            `已经浏览的页面中有 ${lacked.length} 页图片尚未加载,` +
            `已经从结果中剔除。\n它们的页码是:\n${lacked}`
        );
    }


    function* genImgURLs() {
        const params = window?.previewParams;
        if (!params) throw new Error(
            "接口为空: window.previewParams"
        );

        let i = -4;
        const
            base = "https://openapi.renrendoc.com/preview/getPreview?",
            query = {
                temp_view: 0,
                jsoncallback: "a",
                callback: "b",
                encrypt: params.encrypt,
                doc_id: params.doc_id,
                get _() { return Date.now() },
                get start() { return i += 5; },
            };
        
        while (true) {
            const keys = Reflect.ownKeys(query);
            yield base + keys.map(
                key => `${key}=${query[key]}`
            ).join("&");
        }
    }


    async function _fetch_preview_urls() {
        let
            is_empty = true,
            switch_counts = 0,
            previews = [];
        
        for (const [i, url] of utils.enumerate(genImgURLs())) {
            const resp = await fetch(url);
                utils.raise_for_status(resp);
                const raw_data = await resp.text(),
                data = raw_data.slice(2, -1),
                img_urls = JSON
                    .parse(data)
                    .data
                    ?.preview_list
                    ?.map(pair => pair.url);
            if (!img_urls) break;

            previews = previews.concat(...img_urls);
            utils.update_popup(`已经请求 ${i + 1} 组图片链接`);
            
            if (is_empty !== (img_urls.length ? false : true)) {
                is_empty = !is_empty;
                switch_counts++;
            }
            if (switch_counts === 2) break;

            await utils.sleep(1000);
        }
        
        const
            params = window.previewParams,
            free = params.freepage || 20,
            base = params.pre || wk$(".page img")[0].src.slice(0, -5),
            free_urls = Array.from(
                utils.range(1, free + 1)
            ).map(
                n => `${base}${n}.gif`
            );

        const urls = free_urls.concat(...previews);
        utils.save_urls(urls);
    }


    function fetch_preview_urls() {
        return utils.run_with_prog(
            3, _fetch_preview_urls
        );
    }


    function help() {
        alert(
            "【捕获】和【请求】图片链接的区别:\n" + 
            " - 【捕获】是从当前已经加载的文档页中提取图片链接\n" + 
            " - 【请求】是使用官方接口直接下载图片链接\n" + 
            " - 【捕获】使用麻烦,但是稳定\n" + 
            " - 【请求】使用简单,速度快,但可能失效"
        );
    }


    /**
     * 人人文档下载策略
     */
    async function renrendoc() {
        utils.create_btns();
        
        utils.onclick(to_page, 1, "跳转到页码");
        utils.onclick(capture_urls, 2, "捕获图片链接");
        utils.onclick(fetch_preview_urls, 3, "请求图片链接");
        utils.onclick(help, 4, "使用说明");

        utils.toggle_btn(2);
        utils.toggle_btn(3);
        utils.toggle_btn(4);
    }

    /**
     * 取得全部图片连接
     * @returns {Array<string>}
     */
    function get_img_urls() {
        const src = wk$("#page1 img")[0]?.src;

        // 适用于图片类型
        if (src) {
            const path = src.split("?")[0].split("/").slice(3, -1).join("/");
            const origin = new URL(location.href).origin;
        
            const urls = window.htmlConfig.fliphtml5_pages.map(obj => {
                const fname = obj.n[0].split("?")[0].split("/").at(-1);
                return `${origin}/${path}/${fname}`;
            });
            const unique = [...new Set(urls)];
        
            window.img_urls = unique;
            return unique;
        }

        // 适用于其他类型
        const relative_path = wk$(".side-image img")[0].getAttribute("src").split("?")[0];
        // ../files/large/
        const relative_dir = relative_path.split("/").slice(0, -1).join("/") + "/";

        const base = location.href;
        const urls = window.htmlConfig.fliphtml5_pages.map(obj => {
            // "../files/large/d8b6c26f987104455efb3ec5addca7c9.jpg"
            const path = relative_dir + obj.n[0].split("?")[0];
            const url = new URL(path, base);
            // https://book.yunzhan365.com/mctl/itid/files/large/d8b6c26f987104455efb3ec5addca7c9.jpg
            return url.href.replace("/thumb/", "/content-page/");
        });

        window.img_urls = urls;
        return urls;
    }


    function imgs_to_pdf() {
        const urls = get_img_urls();
        const title = document.title;
        const task = () => utils.img_urls_to_pdf(urls, title);

        utils.run_with_prog(1, task);
        alert(
            "正在下载图片,请稍等,时长取决于图片数量\n" +
            "如果导出的文档只有一页空白页,说明当前文档不适用"
        );
    }


    /**
     * 将数组中的连续数字描述为字符串
     * 例如 [1, 2, 3, 5] => "1 - 3, 5"
     * @param {number[]} nums 整数数组
     * @returns {string} 描述数组的字符串
     */
    function describe_nums(nums) {
        let result = "";
        let start = nums[0];
        let end = nums[0];
        
        for (let i = 1; i < nums.length; i++) {
            if (nums[i] === end + 1) {
                end = nums[i];
            } else {
                if (start === end) {
                result += start + ", ";
                } else {
                result += start + " - " + end + ", ";
                }
                start = nums[i];
                end = nums[i];
            }
        }
        
        if (start === end) {
            result += start;
        } else {
            result += start + " - " + end;
        }
        
        return result;
    }


    /**
     * 取得总页码(作为str)
     * @returns {string}
     */
    function get_total() {
        const total = window?.bookConfig?.totalPageCount;
        if (total) {
            return String(total);
        }
        return wk$("#tfPageIndex input")[0].value.split("/")[1].trim();
    }


    /**
     * 下载稀疏数组的pdf数据,每个元素应该是 [pdf_blob, pwd_str]
     * @param {Array} pdfs_data 
     */
    async function data_to_zip(pdfs_data) {
        // 导入jszip
        await utils.blobs_to_zip([], "empty", "dat", "empty", false);

        // 分装截获的数据
        const page_nums = Object.keys(pdfs_data)
            .map(index => parseInt(index) + 1);
        const len = page_nums.length;
        const pwds = new Array(len + 1);
        pwds[0] = "page-num,password";
        
        // 创建压缩包,归档加密的PDF页面
        const zip = new window.JSZip();
        const total = get_total();
        const digits = total.length;

        // 归档
        for (let i = 0; i < len; i++) {
            // 页码左侧补零
            const page_no = page_nums[i];
            const page_no_str = page_no.toString().padStart(digits, "0");
            // 记录密码
            pwds[i+1] = `${page_no_str},${pdfs_data[page_no - 1][1]}`;
            // 添加pdf内容到压缩包
            const blob = pdfs_data[page_no - 1][0];
            zip.file(`page-${page_no_str}.pdf`, blob, { binary: true });
        }
        console.log("zip:", zip);

        // 添加密码本到压缩包
        const pwds_blob = new Blob([pwds.join("\n")], { type: "text/plain" });
        zip.file(`密码本.txt`, pwds_blob, { binary: true });
        
        // 下载
        console.info("正在合成压缩包并导出,请耐心等待几分钟......");
        const zip_blob = await zip.generateAsync({ type: "blob" });
        utils.save(`${document.title}.zip`, zip_blob, "application/zip");
    }


    /**
     * 下载多个pdf为一个压缩包,其中包含一个密码本
     * @param {PointerEvent} event
     */
    async function export_zip(event) {
        // 异常判断
        if (!window.pdfs_data) utils.raise(`pdfs_data 不存在!`);

        // 确认是否继续导出PDF
        const page_nums = Object.keys(pdfs_data)
            .map(index => parseInt(index) + 1);
        const donwload = confirm(
            `已经捕获 ${page_nums.length} 个页面,是否导出?\n` +
            `已捕获的页码:${describe_nums(page_nums)}\n` + 
            `(如果某页缺失可以先多向后翻几页,然后翻回来,来重新加载它)`
        );
        if (!donwload) return;
        
        // 隐藏按钮
        const btn = event.target;
        btn.style.display = "none";

        // 下载压缩包
        await data_to_zip(pdfs_data);

        // 显示按钮
        btn.style.display = "block";
    }


    function steal_pdf_when_page_loaded() {
        // 共用变量
        // 存放pdf数据,[[<pdf_blob>, <pwd_str>], ...]
        window.pdfs_data = [];
        // 代表当前页码
        let page_no = NaN;

        // hook PdfLoadingTask.prototype.start
        const _start = PdfLoadingTask.prototype.start;
        wk$._start = _start;
        PdfLoadingTask.prototype.start = function() {
            // 取得页码
            page_no = this.index;

            // 如果不存在此页,则准备捕获此页面
            if (!pdfs_data[page_no - 1]) {
                pdfs_data[page_no - 1] = [];
            }
            
            return _start.call(this);
        };

        // hook getBlob
        const _get_blob = getBlob;
        wk$._get_blob = _get_blob;
        window.getBlob = async function(param) {
            const result = await _get_blob.call(this, param);
            // 如果当前页面需要捕获,则设置对应项的密码
            if (page_no > 0) {
                const resp = await fetch(result.url);
                const blob = await resp.blob();

                pdfs_data[page_no - 1] = [blob, result.password];
                page_no = NaN;
            } 
            return result;
        };

        utils.onclick(export_zip, 1, "导出PDF压缩包");
    }


    /**
     * 请求 url 并将资源转为 [pdf_blob, password_str]
     * @param {string} url
     * @returns {Array} 
     */
    async function url_to_item(url) {
        // 取得pdf数据
        const resp = await fetch(url);
        const buffer = await resp.arrayBuffer();
        const bytes = new Uint8Array(buffer);
        const len = bytes.length;

        // 更新进度
        window.downloaded_count++;
        window.downloaded_size += len;
        console.log(
            `已经下载了 ${downloaded_count} 页,\n` +
            `累计下载了 ${(downloaded_size / 1024 / 1024).toFixed(1)} MB`
        );

        // 取出密钥
        const pwd = new Uint8Array(6);
        pwd.set(bytes.subarray(1080, 1083));
        pwd.set(bytes.subarray(-1003, -1000), 3);
        const pwd_str = new TextDecoder().decode(pwd);

        // 解密出数据
        const pdf = bytes.subarray(1083, -1003);
        pdf.subarray(0, 4000).forEach((byte, i) => {
            pdf[i] = 255 - byte;
        });
        return [
            new Blob([pdf, pdf.subarray(4000)], { type: "application/pdf" }),
            pwd_str
        ];
    }


    /**
     * 直接下载并解析原始数据,导出PDF压缩包
     * @param {PointerEvent} event
     */
    async function donwload_zip(event) {
        // 隐藏按钮
        const btn = event.target;
        btn.style.display = "none";
        
        // 共用进度变量
        window.downloaded_count = 0;
        window.downloaded_size = 0;

        // 取得数据地址
        const urls = get_img_urls()
            .map(url => url.replace("/thumb/", "/content-page/"));
        // 批量下载
        const item_tasks = urls.map(url_to_item);
        const items = await utils.gather(item_tasks);
        // 导出ZIP
        await data_to_zip(items);

        // 显示按钮
        btn.style.display = "block";
    }


    /**
     * 导出图片到PDF
     */
    function judge_file_type() {
        const ext = window
            ?.htmlConfig
            ?.fliphtml5_pages[0]
            ?.n[0]
            ?.split("?")[0]
            ?.split(".").at(-1);

        console.log("ext:", ext);

        if (["zip"].includes(ext)
            && window?.PdfLoadingTask
            && window?.getBlob) {

            utils.onclick(steal_pdf_when_page_loaded, 1, "开始捕获");
            utils.onclick(donwload_zip, 2, "下载PDF压缩包");
            utils.toggle_btn(2);
        }
        else if (wk$("#page1 img")[0]) {
            utils.onclick(imgs_to_pdf, 1, "导出PDF");
        }
        else {
            utils.onclick(() => null, 1, "此文档不适用");
        }
    }


    /**
     * 云展网文档下载策略
     */
    async function yunzhan365() {
        // 根据网址分别处理
        if (location.pathname.startsWith("/basic")) {
            return;
        }

        // 创建脚本启动按钮
        utils.create_btns();
        judge_file_type();
    }

    /**
     * 导出图片链接
     */
    function exportURLs$1() {
        const all = parseInt(
            wk$("[class*=total]")[0]
        );
        const imgs = wk$("[class*=imgContainer] img");
        const got = imgs.length;

        if (got < all) {
            if (!confirm(
                `当前浏览页数:${got},总页数:${all}\n建议浏览剩余页面以导出全部链接\n是否继续导出链接?`
            )) {
                return;
            }
        }
        utils.save_urls(
            imgs.map(img => img.src)
        );
    }


    /**
     * 360文库文档下载策略
     */
    function wenku360() {
        utils.create_btns();
        utils.onclick(
            exportURLs$1, 1, "导出图片链接"
        );

        // utils.onclick(
        //     callAgent, 2, "导出PDF"
        // );
        // utils.toggle_btn(2);
    }

    async function getFileInfo() {
        const
            uid = new URL(location.href).searchParams.get("contentUid"),
            resp = await fetch("https://zyjy-resource.webtrn.cn/sdk/api/u/open/getResourceDetail", {
                "headers": {
                    "accept": "application/json, text/javascript, */*; q=0.01",
                    "content-type": "application/json",
                },
                "referrer": "https://jg.class.com.cn/",
                "body": `{"params":{"contentUid":"${uid}"}}`,
                "method": "POST",
            }),
            data = await resp.json(),
            url = data["data"]["downloadUrl"],
            fname = data["data"]["title"];

        let ext;
        try {
            // validate the URL format 
            // and get the file format
            ext = new URL(url).pathname.split(".").at(-1);
        } catch(e) {
            console.log(data);
            throw new Error("API changed, the script is invalid now.");
        }
        return { url, fname, ext };
    }


    /**
     * 保存文件
     * @param {{fname: string, url: string, ext: string}} info 
     */
    async function saveFile(info) {
        const
            resp = await fetch(info.url),
            blob = await resp.blob();
        utils.save(info.fname + `.${info.ext}`, blob);
    }


    /**
     * 劫持保存网页,改为保存文件
     * @param {KeyboardEvent} e 
     */
    function onCtrlS(e) {
        if (e.code === "KeyS" &&
            e.ctrlKey) {
            console.log("ctrl + s is captured!!");
            getFileInfo().then(info => saveFile(info));

            e.preventDefault();
            e.stopImmediatePropagation();
            e.stopPropagation();
        }
    }


    /**
     * 技工教育网文档策略
     */
    function jg() {
        window.addEventListener(
            "keydown", onCtrlS, true
        );
    }

    async function estimateTimeCost() {
        wk$(".w-page").at(-1).scrollIntoView();
        await utils.sleep(1000);

        let total = wk$("#pageNumber-text")[0].textContent.split("/")[1];
        total = parseInt(total);
        return confirm([
            "注意,一旦开始截图就无法停止,除非刷新页面。",
            "浏览器窗口最小化会导致截图提前结束!",
            "建议将窗口最大化,这将【显著增大清晰度和文件体积】",
            `预计耗时 ${1.1 * total} 秒,是否继续?`,
        ].join("\n"));
    }


    /**
     * 逐页捕获canvas
     * @returns {Promise<Array<Blob>>}
     */
    async function collectAll() {
        const imgs = [];
        let div = wk$(".w-page")[0];
        let i = 0;
        
        while (true) {
            // 取得 div
            const anchor = Date.now();
            while (!div && (Date.now() - anchor < 1000)) {
                console.log(`retry on page ${i+1}`);
                await utils.sleep(200);
            }
            if (!div) throw new Error(
                `can not fetch <div>: page ${i}`
            );
            
            // 移动到 div
            div.scrollIntoView({ behavior: "smooth" });
            await utils.sleep(1000);
            
            // 取得 canvas
            let canvas = wk$.call(div, "canvas")[0];
            let j = 0;
            while (!canvas && j < 100) {
                div = div.nextElementSibling;
                canvas = wk$.call(div, "canvas")[0];
                j++;
            }
            if (!div)  throw new Error(
                `can not fetch <div>: page ${i}*`
            );

            // 存储 canvas
            imgs.push(
                await utils.canvas_to_blob(canvas)
            );
            console.log(`canvas stored: ${++i}`);

            // 下一轮循环
            div = div.nextElementSibling;
            if (!div) break;
        }
        console.log("done");
        return imgs;
    }


    /**
     * 放大或缩小文档画面
     * @param {boolean} up 
     */
    async function scale(up) {
        let s = "#magnifyBtn";
        if (!up) {
            s = "#shrinkBtn";
        }
        const btn = wk$(s)[0];
        for (let _ of utils.range(10)) {
            btn.click();
            await utils.sleep(500);
        }
    }


    /**
     * 获取全部canvas,显示功能按钮
     * @returns 
     */
    async function prepare() {
        if (! await estimateTimeCost()) {
            return;
        }

        // 隐藏按钮
        utils.toggle_btn(1);
        // 放大画面
        await scale(true);

        let imgs;
        try {
            imgs = await collectAll();
        } catch(e) {
            console.error(e);
        } finally {
            // 缩小画面
            scale(false);
        }
        
        // window.imgs = imgs;
        // 显示功能按钮
        const fname = "技工教育网文档";
        utils.onclick(
            () => utils.img_blobs_to_pdf(imgs, fname),
            2,
            "导出PDF"
        );
        utils.toggle_btn(2);

        utils.onclick(
            () => utils.blobs_to_zip(imgs, "page", "png", fname),
            3,
            "导出ZIP"
        );
        utils.toggle_btn(3);
    }


    /**
     * 技工教育文档预览页面策略
     */
    function jgPreview() {
        utils.create_btns();
        utils.onclick(
            prepare, 1, "截图文档"
        );
    }

    /**
     * 取得文档标题
     * @returns {string}
     */
    function getTitle() {
        return document.title.slice(0, -4);
    }


    /**
     * 取得基础URL
     * @returns {string}
     */
    function getBaseURL$1() {
        return wk$("#dp")[0].value;
    }


    /**
     * 获取总页码
     * @returns {number}
     */
    function getTotalPageNum() {
        const num = wk$(".shop3 > li:nth-child(3)")[0]
            .textContent
            .split("/")[1]
            .trim();
        return parseInt(num);
    }


    /**
     * 返回图片链接生成器
     * @param {string} base 基础图片链接地址
     * @param {number} max 最大数量
     * @returns {Generator<string, void, unknown>}
     */
    function* imgURLsMaker(base, max) {
        for (let i of utils.range(1, max + 1)) {
            yield `${base}${i}.gif`;
        }
    }


    /**
     * 取得当前页面全部图片链接(生成器)
     * @returns {Generator<string, void, unknown>}
     */
    function getImgURLs() {
        const
            base = getBaseURL$1(),
            total = getTotalPageNum();
        return imgURLsMaker(base, total)
    }


    function exportPDF() {
        const urls = getImgURLs();
        const title = getTitle();
        return utils.run_with_prog(
            2, () => utils.img_urls_to_pdf(urls, title)
        );
    }


    function exportURLs() {
        const urls = getImgURLs();
        utils.save_urls(urls);
    }


    /**
     * 文库吧文档下载策略
     */
    function wenkub() {
        utils.create_btns();
        
        utils.onclick(
            exportURLs, 1, "导出图片链接"
        );

        utils.onclick(
            exportPDF, 2, "导出PDF(测试)"
        );
        utils.toggle_btn(2);
    }

    const KEY = "5zAUzyJv5xLoYyCCBJdxVw==";


    function* pageURLGen() {
        const
            url = new URL(location.href),
            params = url.searchParams,
            base = url.origin + (window.basePath || "/manuscripts/pdf"),
            type = params.get("type") || "pdf",
            id = params.get("id")
                || new URL(wk$("#pdfContent")[0].src).searchParams.get("id")
                || utils.raise("书本ID未知");
        
        let i = 0;
        let cur_url = "";
        
        if (window.wk_sklib_url) {
            console.log(`sklib 使用自定义 url: ${window.wk_sklib_url}`);

            while (true) {
                cur_url = window.wk_sklib_url.replace("{id}", id).replace("{index}", `${i}`);
                yield [i, cur_url];
                console.log("wk: target:", cur_url);
                i++;
            }
        } else {
            while (true) {
                cur_url = `${base}/data/${type}/${id}/${i}?random=null`;
                yield [i, cur_url];
                console.log("wk: target:", cur_url);
                i++;
            }
        }

    }


    async function get_bookmarks() {
        const url = new URL(location.origin);
        const id = utils.get_param("id");
        url.pathname = `/manuscripts/pdf/catalog/pdf/${id}`;
        const resp = await fetch(url.href);
        const data = await resp.json();
        const bookmarks = JSON.parse(data.data).outline;
        return bookmarks;
    }


    async function save_bookmarks() {
        const bookmarks = await get_bookmarks();
        const text = JSON.stringify(bookmarks, null, 2);
        utils.save("bookmarks.json", text, { type: "application/json" });
    }


    function decrpyt_pdf_data(encrypted_b64_data, b64_key) {
        console.info("CryptoJS:", window.CryptoJS);

        const key = CryptoJS.enc.Base64.parse(b64_key);
        const decrypted = CryptoJS.AES.decrypt(encrypted_b64_data, key, {
            mode: CryptoJS.mode.ECB,
            padding: CryptoJS.pad.Pkcs7,
        });
        const decrypted_b64 = CryptoJS.enc.Base64.stringify(decrypted).toString();
        return atob(decrypted_b64);
    }

    /**
     * 下载所有pdf文件数据,返回字节串数组
     * @returns {Promise<Array<Uint8Array>>}
     */
    async function fetch_all_pdfs() {
        // 如果已经下载完成,则直接返回之前的结果
        if (window.download_finished) {
            return window.pdfs;
        }

        // 显示进度的按钮
        const prog_btn = utils.btn(3);
        window.download_finished = false;

        // 存储pdf字节串
        const pdfs = [];
        let
            last_digest = NaN,
            size = NaN;

        // 读取每个PDF的页数
        if (window.loadPdfInfo) {
            try {
                const resp = await loadPdfInfo();
                const info = JSON.parse(resp.data);
                size = parseInt(info.size) || size;
            } catch(e) {
                console.error(e);
            }
        }

        for (const [i, url] of pageURLGen()) {
            // 取得数据
            const b64_data = await fetch(url).then(resp => resp.text());
            // 如果获取完毕,则退出
            if (!b64_data.length) break;
            // 计算摘要
            const digest = utils.crc32(b64_data);
            // 如果摘要重复了,说明到达最后一页,退出
            if (digest === last_digest) break;
            // 否则继续
            last_digest = digest;
            const decrypted_b64_data = decrpyt_pdf_data(b64_data, KEY);
            const decrpyt_data = utils.b64_to_bytes(decrypted_b64_data);
            pdfs.push(decrpyt_data);

            // 更新进度
            const progress = `已经获取 ${i + 1} 组页面,每组`
                + (size ? ` ${size} 页` : '页数未知');
            console.info(progress);
            prog_btn.textContent = `${i + 1} 组 / ${size} 页`;
        }

        window.pdfs = pdfs;
        window.download_finished = true;
        return pdfs;
    }


    /**
     * @param {Function} async_fn 
     * @returns {Function}
     */
    function toggle_dl_btn_wrapper(async_fn) {
        return async function(...args) {
            utils.toggle_btn(1);
            utils.toggle_btn(2);
            await async_fn(...args);
            utils.toggle_btn(1);
            utils.toggle_btn(2);
        }
    }


    async function download_pdf$1() {
        alert(
            "如果看不到进度条请使用开发者工具(F12)查看日志\n" +
            "如果文档页数过多可能导致合并PDF失败\n" +
            "此时请使用【下载PDF数据集】按钮"
        );

        const pdfs = await fetch_all_pdfs();
        const combined = await utils.join_pdfs(pdfs);
        utils.save(
            document.title + ".pdf",
            combined,
            "application/pdf"
        );
        utils.btn(3).textContent = "进度条";
    }

    download_pdf$1 = toggle_dl_btn_wrapper(download_pdf$1);


    async function download_data_bundle() {
        alert(
            "下载的是 <文档名称>.dat 数据集\n" +
            "等价于若干 PDF 的文件顺序拼接\n" +
            "请使用工具切割并合并为一份 PDF\n" +
            "工具(pdfs-merger)链接在脚本主页"
        );

        const pdfs = await fetch_all_pdfs();
        const blob = new Blob(pdfs, { type: "application/octet-stream" });
        const url = URL.createObjectURL(blob);
        
        const a = document.createElement("a");
        a.download = document.title + ".dat";
        a.href = url;
        a.click();

        URL.revokeObjectURL(url);
        console.log("pdf数据集", blob);
    }

    download_data_bundle = toggle_dl_btn_wrapper(download_data_bundle);


    function sdlib() {
        const url = new URL(location.href);
        const encrypted_id = url.pathname.split("/")[2];
        window.basePath = `/https/${encrypted_id}${basePath}`;
    }


    /**
     * 钩子函数,启动于主函数生效时,便于不同网站微调
     */
    function load_hooks() {
        const host_to_fn = {
            "gwfw.sdlib.com": sdlib,
        };
        const fn = host_to_fn[location.hostname];
        if (fn) {
            // 如果存在对应 hook 函数,则调用,否则忽略
            fn();
        }
    }


    /**
     * 中国社会科学文库文档策略
     */
    function sklib() {
        // 如果存在 pdf iframe 则在 iframe 中调用自身
        const iframe = wk$("iframe#pdfContent")[0];
        if (iframe) return;
        
        // 加载钩子,方便适应不同网站
        load_hooks();

        // 创建按钮区
        utils.create_btns();
        // 设置功能
        utils.onclick(download_pdf$1, 1, "下载PDF");
        utils.onclick(download_data_bundle, 2, "下载PDF数据集");
        utils.onclick(() => false, 3, "进度条");
        utils.onclick(save_bookmarks, 4, "下载书签");
        // 显示按钮
        utils.toggle_btn(2);
        utils.toggle_btn(3);
        utils.toggle_btn(4);
        // 设置按钮样式
        utils.btn(3).style.pointerEvents = "none";
    }

    /**
     * 返回基础图片地址,接上 <页码>.gif 即为完整URL
     * @returns {string}
     */
    function getBaseURL() {
        const
            elem = wk$("#page_1 img")[0],
            src = elem.src;

        if (!src) {
            alert("当前页面不能解析!");
            return;
        }
        if (!src.endsWith("1.gif")) {
            alert("当前文档不能解析!");
            throw new Error("第一页图片不以 1.gif 结尾");
        }
        return src.slice(0, -5);
    }


    function* imgURLGen() {
        const
            base = getBaseURL(),
            max = parseInt(
                // ' / 23 '
                wk$(".counts")[0].textContent.split("/")[1]
            );

        for (const i of utils.range(1, max + 1)) {
            yield `${base}${i}.gif`;
        }
    }


    function getURLs() {
        utils.save_urls(
            imgURLGen()
        );
    }


    function jinchutou() {
        utils.create_btns();
        utils.onclick(
            getURLs, 1, "导出图片链接"
        );
    }

    // http://www.nrsis.org.cn/mnr_kfs/file/read/55806d6159b7d8e19e633f05fa62fefa


    function get_pdfs() {
        // 34
        const size = window?.Page.size;
        if (!size) utils.raise("无法确定总页码");

        // '/mnr_kfs/file/readPage'
        const path = window
            ?.loadPdf
            .toString()
            .match(/url:'(.+?)',/)[1];
        if (!path) utils.raise("无法确定PDF路径");

        const code = location.pathname.split("/").at(-1);

        const tasks = [...utils.range(1, size + 1)].map(
            async i => {
                const resp = await fetch(path + "?wk=true", {
                    "headers": {
                        "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
                    },
                    "body": `code=${code}&page=${i}`,
                    "method": "POST",
                });

                if (!resp.ok) utils.raise(`第 ${i} 页获取失败!`);
                utils.update_popup(`已经获取第 ${i} 页`);

                const b64_str = await resp.text();
                return utils.b64_to_bytes(b64_str);
            }
        );
        return utils.gather(tasks);
    }


    function get_title() {
        return document.title.slice(0, -5);
    }


    function download_pdf() {
        utils.run_with_prog(1, async () => {
            const pdfs = await get_pdfs();
            debugger;
            const pdf = await utils.join_pdfs(pdfs);
            utils.save(
                get_title(), pdf, "application/pdf"
            );
        });
    }


    function add_style() {
        const style = `
    <style>
        #nprogress .nprogress-spinner-icon.forbidden {
            border-top-color: #b171ff;
            border-left-color: #bf8aff;
            animation: nprogress-spinner 2.4s linear infinite;
        }
    </style>
    `;
        document.body.insertAdjacentHTML(
            "beforeend", style
        );
    }


    function init_forbid_origin_pdf_fetch() {
        console.log("hooked xhr.open");

        // 修改转圈图标
        wk$(".nprogress-spinner-icon")[0]
            .classList.add("forbidden");

        const open = XMLHttpRequest.prototype.open;

        // 重写 XMLHttpRequest.prototype.open 方法
        XMLHttpRequest.prototype.open = function() {
            const args = Array.from(arguments);
            const url = args[1];

            if (!(url.includes("readPage") &&
                !url.includes("wk=true")
            )) return;
            
            this.send = () => undefined;
            open.apply(this, args);
        };

        return function regain_open() {
            const url = new URL(location.href);
            url.searchParams.set("intercept", "0");
            location.assign(url.toString());
        }
    }


    /**
     * nrsis 文档策略
     */
    function nrsis() {
        utils.create_btns();
        utils.onclick(download_pdf, 1, "下载PDF");
        
        if (!utils.get_param("intercept")) {
            add_style();
            const regain_open = init_forbid_origin_pdf_fetch();
            utils.onclick(regain_open, 2, "恢复页面加载");
            utils.toggle_btn(2);
        }
    }

    // ==UserScript==
    // @name         先晓书院PDF下载
    // @namespace    http://tampermonkey.net/
    // @version      0.1
    // @description  先晓书院PDF下载,仅对PDF预览有效
    // @author       2690874578@qq.com
    // @match        https://xianxiao.ssap.com.cn/index/rpdf/read/id/*/catalog_id/0.html?file=*
    // @require      https://greasyfork.org/scripts/445312-wk-full-cli/code/wk-full-cli.user.js
    // @icon         https://www.google.com/s2/favicons?sz=64&domain=xianxiao.ssap.com.cn
    // @grant        none
    // @run-at       document-idle
    // @license      GPL-3.0-only
    // ==/UserScript==



    /**
     * @param {number} begin 
     * @param {number} end 
     * @param {() => void} onload 
     * @returns {Promise<ArrayBuffer>}
     */
    async function fetch_file_chunk(url, begin, end, onload) {
        const resp = await fetch(url, {
            headers: { "Range": `bytes=${begin}-${end}` }
        });
        const buffer = await resp.arrayBuffer();
        onload();
        return buffer;
    }


    /**
     * 取得文档 ID
     * @returns {string}
     */
    function make_pdf_url() {
        const get_value = (key) => {
            const regex = new RegExp(`(?<=${key}=)[0-9]+`);
            return location.search.match(regex)[0];
        };
        const id = get_value("id");
        const catalog_id = get_value("catalog_id");
        return `${location.origin}/rpdf/pdf/id/${id}/catalog_id/${catalog_id}.pdf`;
    }


    /**
     * @param {string} url 
     * @returns {Promise<number>}
     */
    async function get_file_size(url) {
        const resp = await fetch(url, {
            headers: { "Range": `bytes=0-1` }
        });
        const size_text = resp.headers.get("content-range").split("/")[1];
        return parseInt(size_text);
    }


    /**
     * @param {PointerEvent} event 
     */
    async function export_pdf(event) {
        const btn = event.target;

        // 准备请求
        const url = make_pdf_url();
        const size = await get_file_size(url);
        const chunk = 65536;
        const times = Math.floor(size / chunk);
        
        // 准备进度条
        let finished = 0;
        const update_progress = () => {
            finished++;
            const loaded = ((finished * chunk) / 1024 / 1024).toFixed(2);
            const text = `已下载 ${loaded} MB`;
            utils.print(`chunk<${finished}>:`, text);
            btn.textContent = text;
        };

        // 分片请求PDF
        const tasks = [];
        for (let i = 0; i < times; i++) {
            tasks[i] = fetch_file_chunk(
                url,
                i * chunk,
                (i + 1) * chunk - 1,
                update_progress,
            );
        }

        // 请求最后一片
        const tail = size % chunk;
        tasks[times] = fetch_file_chunk(
            url,
            size - tail,
            size - 1,
            update_progress,
        );

        // 等待下载完成
        const buffers = await utils.gather(tasks);
        utils.print("--------全部下载完成--------");
        utils.print("全部数据分片:", { get data() { return buffers; } });

        // 导出PDF
        const blob = new Blob(buffers);
        const fname = top.document.title.split("_")[0] + ".pdf";
        utils.save(fname, blob, "application/pdf");
    }


    /**
     * 先晓书院 文档策略
     */
    function xianxiao() {
        utils.print("进入<先晓书院PDF下载>脚本");
        utils.create_btns();
        utils.onclick(export_pdf, 1, "下载PDF");
    }

    function hook_log() {
        // 保证 console.log 可用性
        const con = window.console;
        const { log, info, warn, error } = con;

        // 对于 console.log 能 hook 则 hook
        if (Object.getOwnPropertyDescriptor(window, "console").configurable
        && Object.getOwnPropertyDescriptor(con, "log").configurable) {
            // 保证 console 不能被改写
            Object.defineProperty(window, "console", {
                get: function() { return con; },
                set: function(value) {
                    log.call(con, "window.console 想改成", value, "?没门!");
                },
                enumerable: false,
                configurable: false,
            });

            // 保证日志函数不被改写
            const fn_map = { log, info, warn, error };
            Object.getOwnPropertyNames(fn_map).forEach((prop) => {
                Object.defineProperty(con, prop, {
                    get: function() { return fn_map[prop]; },
                    set: function(value) {
                        log.call(con, `console.${prop} 想改成`, value, "?没门!");
                    },
                    enumerable: false,
                    configurable: false,
                });
            });
        }
    }


    /**
     * 主函数:识别网站,执行对应文档下载策略
     */
    function main(host=null) {
        // 绑定函数到全局
        window.wk_main = main;

        // 显示当前位置
        host = host || location.hostname;
        const url = new URL(location.href);
        const params = url.searchParams;
        const path = url.pathname;

        hook_log();    
        console.log(`当前 host: ${host}\n当前 url: ${url.href}`);

        if (host.includes("docin.com")) {
            docin();
        } else if (host === "swf.ishare.down.sina.com.cn") {
            if (params.get("wk") === "true") {
                ishareData2();
            } else {
                ishareData();
            }
        } else if (host.includes("ishare.iask")) {
            ishare();
        } else if (host === "www.deliwenku.com") {
            deliwenku();
        } else if (host.includes("file") && host.includes("deliwenku.com")) {
            deliFile();
        } else if (host === "www.doc88.com") {
            doc88();
        } else if (host === "www.360doc.com") {
            doc360();
        } else if (host === "doc.mbalib.com") {
            mbalib();
        } else if (host === "www.dugen.com") {
            dugen();
        } else if (host === "c.gb688.cn") {
            gb688();
        } else if (host === "www.safewk.com") {
            safewk();
        } else if (host.includes("book118.com")) {
            book118();
        } else if (host === "www.renrendoc.com") {
            renrendoc();
        } else if (host.includes("yunzhan365.com")) {
            yunzhan365();
        } else if (host === "wenku.so.com") {
            wenku360();
        } else if (host === "jg.class.com.cn") {
            jg();
        } else if (host === "preview.imm.aliyuncs.com") {
            jgPreview();
        } else if (host === "www.wenkub.com") {
            wenkub();
        } else if (
            (host.includes("sklib") && path === "/manuscripts/")
            || host === "gwfw.sdlib.com") {
            sklib();
        } else if (host === "www.jinchutou.com") {
            jinchutou();
        } else if (host === "www.nrsis.org.cn") {
            nrsis();
        } else if (host === "xianxiao.ssap.com.cn") {
            xianxiao();
        } else {
            console.log("匹配到了无效网页");
        }
    }


    setTimeout(main, 1000);

})();