您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
方便地打印或以 PDF 形式导出微信公众号文章,让您一键开卷!
// ==UserScript== // @name 微信公众号推文打印脚本 // @namespace mem.ac/weixin-print-to-pdf // @version 1.6.4 // @description 方便地打印或以 PDF 形式导出微信公众号文章,让您一键开卷! // @author memset0 // @license AGPL-v3.0 // @match https://mp.weixin.qq.com/s* // @updateurl https://cdn.jsdelivr.net/gh/memset0/weixin-print-to-pdf/index.js // @downloadurl https://cdn.jsdelivr.net/gh/memset0/weixin-print-to-pdf/index.js // @run-at document-start // ==/UserScript== const CSS = ` .mem-print-container { } .mem-print-settings { margin: auto; padding: 16px; font-size: 13px; line-height: 24px; letter-spacing: -.2px; } .mem-print-settings-title { font-weight: bold; font-size: 21px; margin-top: 12px; margin-bottom: 12px; } .mem-print-settings-title a { color: red; font-size: 13px; } .mem-print-settings-btn-group { margin-top: 8px; margin-bottom: 4px; } .mem-print-settings-btn-group button { margin-right: 12px; padding: 2px 4px; } .mem-print-filter-applied { background: rgba(255, 0, 0, .3); border-left: 5px solid red; } #mem-print-main { line-height: 0px; margin-bottom: 20px; /* padding: 16px; border: 1px solid #D9DADC; */ } #mem-print-main button { margin-right: 8px; } `; function log(...args) { console.log('[@memset0/weixin-print-to-pdf]', ...args); } function isInteger(value) { const converted = +value; return !isNaN(converted) && Number.isInteger(converted); } function applyFilter(iterable, filterPattern) { const illegalFilter = (msg) => (alert('Illegal filter: ' + String(msg)), []); const flag = []; for (const _ in iterable) { flag.push(false); } if (!filterPattern || filterPattern == '-') { for (const i in flag) { flag[+i] = true; } } else { const filters = filterPattern.split(','); for (const filter of filters) { if (filter.includes('-')) { const splited = filter.split('-'); if (splited.length > 2) { return illegalFilter('wrong interval'); } if (!splited[0]) { splited[0] = 0; } if (!splited[1]) { splited[1] = iterable.length - 1; } if (!isInteger(splited[0]) || !isInteger(splited[1])) { return illegalFilter('not a number'); } for (let i = +splited[0] - 1; i < +splited[1]; i++) { if (i < 0 || i >= flag.length) { return illegalFilter('out of range'); } flag[i] = true; } } else { if (!isInteger(filter)) { return illegalFilter('not a number'); } const x = +filter - 1; if (x < 0 || x >= flag.length) { return illegalFilter('out of range'); } flag[x] = true; } } } log('apply filter:', filter, flag, iterable); const result = []; for (const i in iterable) { if (flag[+i]) { result.push(iterable[+i]); } } return result; } function applyFilterJS(filterScript) { return eval('(element, index) => {' + filterScript + '};'); } function scrollTo(type) { const scrollSpeed = 50; if (type !== 'top' && type !== 'bottom') { throw new Error('type error!'); } const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight; let promiseResolve = null; let lastTimestamp = null; let scrollRecords = []; function scrollAnimated(timestamp) { const currentScroll = document.documentElement.scrollTop || document.body.scrollTop; scrollRecords.push(currentScroll); if (scrollRecords.length > 5) { scrollRecords.shift(); let finishedFlag = true; for (let i = 1; i < scrollRecords.length; i++) { if (scrollRecords[i] !== scrollRecords[i - 1]) { finishedFlag = false; break; } } if (finishedFlag) { // log('finish', scrollRecords, finishedFlag); return promiseResolve(type); } } if (lastTimestamp === null) { lastTimestamp = timestamp; } else { const deltaTimestamp = timestamp - lastTimestamp; lastTimestamp = timestamp; window.scrollTo(0, currentScroll + scrollSpeed * (type === 'top' ? -deltaTimestamp : +deltaTimestamp)); log(type, currentScroll, scrollHeight, deltaTimestamp); } window.requestAnimationFrame(scrollAnimated); } return new Promise((resolve) => { promiseResolve = resolve; window.requestAnimationFrame(scrollAnimated); }); } async function printToPdf(options, html) { const { width, height, margin } = options; log('print to pdf', width, height, margin); // await scrollTo('top'); // await scrollTo('bottom'); const pixeledMargin = String(margin).split(' ').map((s) => (s + 'px')).join(' '); const printStyle = '<style> /* normalize browsers */ html, body { margin: 0 !important; padding: 0 !important; } </style>' + '<style> /* page settings */ @page { size: ' + width + 'px ' + height + 'px; margin: ' + pixeledMargin + '; } </style>' + '<style> div.page { width: ' + (width - margin * 2) + 'px; height: ' + (height - margin * 2) + 'px; } </style>'; html = printStyle + html; const { zoom } = options; if (+zoom !== 1) { html += '<style>body { zoom: ' + zoom + '; }</style>'; } const { customCSS } = options; if (customCSS) { html += '\n\n\n<!-- Custom CSS --><style>' + customCSS + '</style>\n\n\n'; } // const document = unsafeWindow.document; // seemingly needless const blob = new Blob([html], { type: 'text/html;charset=utf-8' }); const blobUrl = URL.createObjectURL(blob); log('blob url:', blobUrl); const $iframe = document.createElement('iframe'); $iframe.style.display = 'none'; $iframe.src = blobUrl; document.body.appendChild($iframe); $iframe.onload = () => { setTimeout(() => { $iframe.focus(); $iframe.contentWindow.print(); }, 1); }; } function generateHtmlFromContent(options) { let html = ''; const funcFilter = applyFilterJS(options.filterJS); for (const $element of applyFilter(document.getElementById('js_content').children, options.filter)) { if (!funcFilter($element)) { html += $element.outerHTML + '\n\n'; } } return html; } function generateHtmlFromPictures(options) { const minimalImageSize = 100; let html = '<style>' + 'div.page { page-break-after: always; display: flex; justify-content: center; align-items: center; }' + 'div.page>img { width: 100%; max-width: 100%; max-height: 100%; }' + 'div.page>img { border: solid 1px #fff0; } /* this line is magic */' + '</style>'; for (const $image of document.getElementById('js_content').querySelectorAll('img')) { const imageSrc = $image.getAttribute('data-src'); const imageWidth = $image.getAttribute('width'); if (!imageSrc) { continue; } if (imageWidth && imageWidth < minimalImageSize) { continue; } html += '<div class="page"><img src="' + imageSrc + '"></div>'; // log(imageWidth, imageSrc); } return html; } class Settings { createElement() { this.$inputs = {}; const $dialog = document.createElement('dialog'); $dialog.innerHTML = ` <h1 class="mem-print-settings-title"> Settings <a target="_blank" href="https://github.com/memset0/weixin-print-to-pdf">(?)</a> </h1> `; $dialog.className = 'mem-print-settings'; for (const name of Object.keys(this.defaults)) { const $label = document.createElement('label'); const $input = document.createElement('input'); this.$inputs[name] = $input; $label.innerText = name; if (this.defaults[name] !== '') { $label.innerText += '(default: ' + this.defaults[name] + ')' } $label.innerText += ': '; $input.name = name; $input.value = this.data[name]; $input.onblur = (event) => { log('update', $input.name, $input.value); this.updates[$input.name] = $input.value; }; $label.appendChild($input); $dialog.appendChild($label); $dialog.appendChild(document.createElement('br')); } const $btnGroup = document.createElement('div'); $btnGroup.className = 'mem-print-settings-btn-group'; $dialog.appendChild($btnGroup); const $resetButton = document.createElement('button'); $resetButton.innerText = 'Reset'; $resetButton.name = 'reset'; $resetButton.onclick = () => this.closeWindow(this.defaults); $btnGroup.appendChild($resetButton); const $cancelButton = document.createElement('button'); $cancelButton.innerText = 'Cancel'; $cancelButton.name = 'cancel'; $cancelButton.onclick = () => this.closeWindow({}); $btnGroup.appendChild($cancelButton); const $submitButton = document.createElement('button'); $submitButton.innerText = 'Submit'; $submitButton.name = 'submit'; $submitButton.onclick = () => this.closeWindow(this.updates); $btnGroup.appendChild($submitButton); return $dialog; } openWindow() { this.updates = {}; for (const name in this.$inputs) { // log('dialog open:', name, this.data[name], this.$inputs[name].value); this.$inputs[name].value = this.data[name]; } this.$element.showModal(); } closeWindow(update = null) { if (update && Object.keys(update).length) { for (const key in update) { this.data[key] = update[key]; } localStorage.setItem(this.storageKey, JSON.stringify(this.data)); } log('dialog closed:', update, this.data); this.$element.close(); } constructor(storageKey = 'mem-print-settings') { this.storageKey = storageKey; this.data = {}; this.defaults = { // Page Settings width: 797, // A4 8.3inch * 11.7inch height: 1123, margin: 0, zoom: 1, // Element filters filter: '', filterJS: '', // Custom style, customCSS: '', }; if (localStorage.getItem(storageKey)) { const storaged = localStorage.getItem(storageKey); if (storaged) { this.data = JSON.parse(storaged); } } for (const key in this.defaults) { if (!Object.keys(this.data).includes(key)) { this.data[key] = this.defaults[key]; } } this.$element = this.createElement(); document.body.appendChild(this.$element); if (typeof this.$element.showModal !== 'function') { this.$element.hidden = true; alert('Your browser doesn\'t support <dialog>, settings may not work.') } } } function renderFilter(options) { const { filter, filterJS } = options; const funcFilter = applyFilterJS(filterJS); const $content = Array.from(document.getElementById('js_content').children); for (const i in $content) { $content[i].classList.add('mem-print-filter-applied'); } for (const i of applyFilter($content.map((_, i) => i), filter)) { if (!funcFilter($content[i])) { $content[i].classList.remove('mem-print-filter-applied'); } } } async function main() { function generateDiv(id, className) { const $div = document.createElement('div'); $div.id = id; $div.className = className; return $div; } function generateButton(buttonName, callback) { const $btn = document.createElement('button'); $btn.innerText = buttonName; $btn.style = 'padding-left: 4px; padding-right: 4px;' $btn.onclick = () => { log('trigger', [buttonName], $btn); callback(); } return $btn; } const settings = new Settings(); log(settings.data); $mainContainer = generateDiv('mem-print-main', 'mem-print-container'); $sideContainer = generateDiv('mem-print-side', 'mem-print-container'); $style = document.createElement('style'); $style.innerHTML = CSS; $mainContainer.appendChild($style); document.getElementsByClassName('qr_code_pc')[0].appendChild($sideContainer); document.getElementById('img-content').parentNode.insertBefore($mainContainer, document.getElementById('img-content')); console.log($mainContainer, $sideContainer); printContent = () => { printToPdf(settings.data, generateHtmlFromContent(settings.data)); } $mainContainer.appendChild(generateButton('Print Content', printContent)); $sideContainer.appendChild(generateButton('Print Content', printContent)); printPictures = () => { printToPdf(settings.data, generateHtmlFromPictures(settings.data)); }; $mainContainer.appendChild(generateButton('Print Pictures', printPictures)); $sideContainer.appendChild(generateButton('Print Pictures', printPictures)); previewFilters = () => { renderFilter(settings.data); }; $mainContainer.appendChild(generateButton('Preview Filters', previewFilters)); $sideContainer.appendChild(generateButton('Preview Filters', previewFilters)); $mainContainer.appendChild(generateButton('Settings', () => { settings.openWindow(); })); $sideContainer.appendChild(generateButton('Settings', () => { settings.openWindow(); })); } document.addEventListener('DOMContentLoaded', main);